šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Reusable HTML Components: Where Every Piece Comes Together

Master building reusable HTML components: registering custom elements, Shadow DOM encapsulation, combining template and slot for structural content, Declarative Shadow DOM for server-rendered components, and the framework-independence this approach provides.

⚔ Total XP: 0|šŸ’» html XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

Reusable HTML Components

Every piece, combined.


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

This lesson is the direct capstone of the template and slot lessons earlier in this module — custom elements are the missing piece that turns 'inert cloneable markup' and 'content projection' into an actual, real, reusable, tag-like component.

1Custom Elements: A Genuinely New HTML Tag

customElements.define('user-card', UserCard), where UserCard extends HTMLElement, registers a genuinely new HTML tag — <user-card> — backed by that class, with the browser automatically invoking lifecycle callbacks like connectedCallback() (when an instance is inserted into the document) and attributeChangedCallback() (when a watched attribute changes) as the component's actual usage in the page changes over time.

This is a real, native mechanism, not a simulation — <user-card> becomes a genuine HTML element usable anywhere standard markup is written, with the same fundamental status as any built-in tag like <button> or <video>.

class UserCard extends HTMLElement {
  connectedCallback() { this.textContent = "Mounted"; }
}
customElements.define("user-card", UserCard);
localhost:3000
āœ“ A Real, Native Tag — Not A SimulationcustomElements.define() creates a genuinely new HTML element with the same standing as any built-in tag.

2Shadow DOM Encapsulation, Populated By template And slot

this.attachShadow({ mode: "open" }) gives the component its own isolated shadow tree, protecting the outer page's CSS from accidentally reaching in and styling internal component markup, and (by default) keeping the component's own internal styles scoped and non-leaking. This encapsulation is what makes a component's internals genuinely private implementation detail, safely reusable across projects without CSS collisions.

Combining directly with the previous two lessons in this module: connectedCallback() typically clones a <template>'s content directly into that shadow root — real, parser-validated structural markup rather than JS-built strings — and that template's own markup includes <slot> elements, letting the component accept and correctly position caller-supplied light-DOM content exactly as covered in the Slot lesson.

connectedCallback() {
  const shadow = this.attachShadow({ mode: "open" });
  shadow.appendChild(tpl.content.cloneNode(true));
}
localhost:3000
āœ“ Three Lessons From This Module, Now CombinedShadow DOM for encapsulation, template for structure, slot for accepting caller content.

3Declarative Shadow DOM: Server-Rendered Components, No JS Wait

A meaningful limitation of the JS-only approach above: the shadow root's content doesn't exist until JavaScript loads and connectedCallback() actually runs — for a server-rendered page, this means a custom element renders empty (or with fallback light-DOM content only) until JS execution catches up, a real, visible delay for anything meant to appear immediately.

Declarative Shadow DOM solves this directly: a <template shadowrootmode="open"> placed as a direct child of the custom element in the *server-rendered HTML itself* is automatically, natively converted into that element's actual shadow root by the browser's HTML parser — no JavaScript execution required for the shadow content to exist and render at first paint. The custom element's JS class can still attach behavior afterward, but the structural, visual content is already there the instant the HTML parses.

<user-card>
  <template shadowrootmode="open">
    <div class="card"><slot name="name"></slot></div>
  </template>
  <span slot="name">Ada Lovelace</span>
</user-card>
localhost:3000
āœ“ Shadow Content At First Paint, Not After JS LoadsA genuine progressive-enhancement improvement for server-rendered custom elements.

4The Real Payoff: Genuine Framework Independence

Because every piece of this pattern — customElements.define(), Shadow DOM, <template>, <slot>, and Declarative Shadow DOM — is a native browser API rather than a framework-specific abstraction, a component built this way works identically whether it's used inside a React application's JSX, a Vue template, a plain server-rendered HTML page, or any future framework not yet invented, with zero adaptation required.

This is a genuinely distinct property from a component built in any single framework's own component model, which is inherently tied to that framework's runtime and rendering approach — a native custom element is, by construction, exactly as portable as HTML itself, which is the entire, durable value proposition of building this way.

<!-- Identical usage in React JSX, Vue templates, or plain HTML -->
<user-card>…</user-card>
localhost:3000
āœ“ As Portable As HTML ItselfNo framework adaptation layer needed — native custom elements work wherever the DOM exists.

5Step-by-Step Breakdown

A Real Component, Without A Framework. <template> gave you inert, cloneable markup. <slot> gave you a way to accept caller content. Custom elements are the piece that ties both into an actual reusable, encapsulated, tag-like component — usable in any project, with no framework runtime required.

A Custom Element Is A Class Registered As A New Tag. customElements.define('user-card', UserCardClass) registers a class extending HTMLElement as a genuinely new HTML tag — <user-card> — with its own lifecycle callbacks (connectedCallback, disconnectedCallback, attributeChangedCallback) the browser invokes automatically as instances are created, inserted, removed, or have watched attributes change.

Registering A Custom Element. What does customElements.define('user-card', UserCard) actually do?

  • →Registers UserCard as the implementation behind a genuinely new HTML tag, <user-card>
  • →Creates a reusable CSS class named user-card
  • →Only works if a specific JS framework is also loaded on the page

Shadow DOM Encapsulates The Component's Internal Structure And Style. this.attachShadow({ mode: 'open' }) gives an element its own isolated internal DOM tree — a shadow root — where the component's internal markup lives, protected from the outer page's CSS reaching in unexpectedly, and (by default) keeping the component's own internal styles from leaking out onto the rest of the page.

What Shadow DOM Isolates. What does attaching a Shadow DOM to a custom element primarily protect against?

  • →The outer page's CSS accidentally styling internal component markup, and the component's own styles leaking onto the page
  • →The component making unauthorized network requests
  • →JavaScript errors occurring anywhere on the page

template + slot: The Structural Content Of A Well-Built Component. Combining everything from this module: connectedCallback clones a <template>'s content into the shadow root, and that template's own markup includes <slot> elements — giving the component real, parser-validated internal structure that also accepts and correctly positions caller-supplied light-DOM content.

Combining template, slot, And Custom Elements. What role does <template> typically play in a well-built custom element like this?

  • →It provides the source markup cloned into the component's shadow root, including its slots
  • →It has no particular relationship to custom elements
  • →It replaces the need for a JavaScript class entirely

Declarative Shadow DOM: Server-Rendering A Component's Shadow Root. Declarative Shadow DOM lets a component's shadow root be expressed directly in server-rendered HTML — a <template shadowrootmode="open"> as a direct child of the custom element — so the shadow content exists immediately on first paint, without waiting for JavaScript to load and run attachShadow() and populate it, meaningfully improving perceived load performance for server-rendered pages using custom elements.

Declarative Shadow DOM's Purpose. What problem does Declarative Shadow DOM solve for server-rendered pages using custom elements?

  • →The component's shadow DOM content is present at first paint, without waiting for JavaScript to run and build it
  • →It eliminates the need for a custom element's JavaScript class entirely
  • →It's unrelated to server-rendering concerns

A Genuinely Framework-Independent, Portable Component. Because customElements.define(), Shadow DOM, <template>, and <slot> are all native browser APIs, a custom element built entirely from them works identically inside a React app, a Vue app, plain server-rendered HTML, or no framework at all — a real, meaningful portability property no framework-specific component format can offer.

The Portability Payoff. Why can a custom element built entirely with native browser APIs be used identically inside a React app, a Vue app, or plain HTML?

  • →It's implemented with native browser APIs, not any specific framework's component model, so it works wherever HTML/DOM works
  • →React and Vue both have identical, special built-in support specifically for this
  • →It's mostly coincidental and works only in specific framework versions

Reusable HTML Components Mastered. You now know how to build a genuinely reusable, encapsulated, framework-independent component using custom elements, Shadow DOM, template cloning, and slot content projection — plus server-rendering its shadow root declaratively for first-paint content.

Mark A Reusable Component. A data-component attribute names a template as a reusable, script-driven component.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

Fully supported.

Accessibility (A11y)

1Shadow DOM Encapsulation Doesn't Exempt A Component's Internal Markup From Accessibility Requirements

Content inside a shadow root still needs correct semantic structure, labeling, and ARIA where applicable — encapsulation is a styling/structural boundary, not an accessibility exemption.

SEO Implications

  • 1

    Declarative Shadow DOM Content Is Present In The Server-Rendered HTML And Crawlable Immediately

    Unlike JS-populated shadow roots, which may not be visible to crawlers that don't fully execute JavaScript, Declarative Shadow DOM content is part of the actual server response, straightforwardly crawlable like any other HTML.

Best Practices

Use Declarative Shadow DOM For Any Custom Element Rendered Server-Side, Not Only JS-Populated Shadow Roots

This closes the first-paint gap where a server-rendered custom element would otherwise appear empty until JavaScript loads and executes.

Keep A Component's Public API — Its Attributes And Named Slots — Deliberately Small And Well-Documented

Exactly like designing any reusable abstraction, a minimal, clear contract between the component and its callers is what actually makes it reusable across projects and teams.

Frequent Bugs

THE BUG

A server-rendered page shows custom elements as empty or unstyled for a noticeable moment before JavaScript finishes loading.

THE FIX

Use Declarative Shadow DOM (<template shadowrootmode="open">) so the shadow content is present in the initial HTML response, not dependent on JS execution.

THE BUG

Caller-supplied content passed into a custom element doesn't appear anywhere inside it.

THE FIX

Verify the component's shadow root actually contains a matching <slot> (named or default) for that content, as covered in the Slot Element lesson.

Real-World Examples

A Complete, Server-Renderable Reusable Component

A <user-card> component combining Declarative Shadow DOM, slots, and a JS class for any additional interactive behavior.

<user-card>
  <template shadowrootmode="open">
    <style>.card{border:1px solid #ccc;padding:1rem;border-radius:8px;}</style>
    <div class="card">
      <slot name="avatar"><img src="/default-avatar.png" alt=""></slot>
      <slot name="name"></slot>
    </div>
  </template>
  <span slot="name">Ada Lovelace</span>
</user-card>

<script>
customElements.define("user-card", class extends HTMLElement {
  connectedCallback() {
    // Shadow root already exists declaratively — just add behavior
    this.shadowRoot.addEventListener("click", () => this.classList.toggle("expanded"));
  }
});
</script>

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Building a custom element's internal markup via innerHTML string concatenation in JS instead of cloning a <template>

shadow.appendChild(tpl.content.cloneNode(true));

The Solution //

Clone a <template>'s content via cloneNode(true), giving the component real, parser-validated structural markup.

The Error //

Server-rendering a page with custom elements but no Declarative Shadow DOM, causing a visible empty-content flash before JS loads

<user-card> <template shadowrootmode="open">…</template> </user-card>

The Solution //

Use <template shadowrootmode="open"> so the shadow content is present in the initial HTML response.

Lesson Glossary

[01]Custom Element

A class-backed, genuinely new HTML tag registered via customElements.define().

Code Preview
class UserCard extends HTMLElement { … }

[02]Shadow DOM

An element's isolated internal DOM tree, encapsulating style and structure.

Code Preview
this.attachShadow({ mode: "open" })

[03]connectedCallback

A lifecycle method invoked when a custom element is inserted into the document.

Code Preview
connectedCallback() { … }

[04]Declarative Shadow DOM

A server-renderable shadow root, present at first paint without JS.

Code Preview
<template shadowrootmode="open">

Continue Learning