Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~22 min· topic 5 of 8

Topic 15.5

Web Components

In one line

Web Components let you make your own HTML tags: a custom element is a class the browser runs for your tag, shadow DOM gives it private markup and styles, and <template> with <slot> lets page authors pass content into it.

Think of it like this

A toaster. You plug it in and press one lever; you don't see or touch the wires inside, and the paint on your kitchen walls can't change the toaster's insides. You can put your own bread in the slot, and it pops out toast. A Web Component is a toaster for the web: a tag with a simple outside (attributes and events), a sealed inside (shadow DOM), and slots where you put your own content.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Web Components
Browser standards for making your own reusable HTML tags with private insides.
Custom element
Your own HTML tag, like <user-badge>, backed by a JavaScript class.
Upgrade
When the browser turns an already-parsed custom tag into a real instance of its class, once the class is defined.
Lifecycle callback
A method the browser calls at a moment in the element's life, such as when it's added to the page.
Shadow DOM
A private DOM tree attached to an element; its markup and styles are separate from the page.
Shadow root
The top of an element's shadow DOM, created with attachShadow.
Light DOM
The ordinary children written inside a custom element's tag by the page author.
template
An HTML element whose content is parsed but not shown or run, ready to be cloned.
slot
A placeholder in shadow DOM where the element's light-DOM children are displayed.
Composed event
An event allowed to travel out of a shadow root into the main page.

Step by step

01Define a class and a tag

Extend HTMLElement and register it. From now on <user-badge> is a real element with your behaviour.

user-badge.jswhole filejavascript
class UserBadge extends HTMLElement {}
customElements.define("user-badge", UserBadge);

02Give it a shadow root

In the constructor, call super() and attach a shadow root. Put the component's private markup and <style> in it.

Give it a shadow rootdiagram
Rendering diagram…

03React to attributes

List the attributes to watch in static observedAttributes. The browser then calls attributeChangedCallback with the old and new values whenever one changes, including from HTML or DevTools.

04Render when connected

connectedCallback is the moment the element joins the page. Render there and add listeners; remove outside listeners in disconnectedCallback.

Render when connecteddiagram
Rendering diagram…

05Take content through slots

Build the inside from a <template> with <slot> placeholders. The page author writes <span slot="title"> and a paragraph inside your tag, and they appear in the right places.

index.htmlwhole filehtml
<template id="card-tpl">
  <style>:host { display: block; border: 1px solid #ccc; }</style>
  <h3><slot name="title">Untitled</slot></h3>
  <slot></slot>
</template>

<info-card>
  <span slot="title">Tip</span>
  <p>Press Run often.</p>
</info-card>

06Talk back with events

Dispatch a CustomEvent with bubbles: true, composed: true so the page can listen on the host or the document. Put data in detail.

Try it yourself

  1. 1

    Watch disconnect

    In the first example, add a disconnectedCallback() { console.log("disconnected"); } method and badge.remove(); at the end. Predict where the new line appears in the console.

  2. 2

    Fill the default

    In the second example, delete the <span slot="title"> line. Predict what the heading shows (look at the slot's fallback content), then run.

  3. 3

    Make it closed

    In the first example, change mode: "open" to "closed". Predict: what is badge.shadowRoot now, and which line breaks? (Keep a reference to root in the class to fix it.)

Code & diagrams

A custom element: lifecycle and encapsulated styles ES2022+ New tab

The tag was already in the page, so `define` upgraded it: constructor, the existing attribute, then connected. The page rule `span { color: red }` didn't reach the shadow span, and `document.querySelectorAll` can't see inside the shadow root.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

constructor
attribute name null -> Asha
connected
attribute name Asha -> Ravi
rgb(0, 128, 128) rgb(255, 0, 0)
1 Hi, Ravi
template, named slots and a composed event New tab

The event started on the shadow button, but the document listener sees `e.target` as `<info-card>`: retargeting hides the component's insides. Remove the last line and press Close yourself.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

title [ <span> ]
[ <p> ]
closed: button <info-card>
Theming through the shadow boundary with custom properties New tab

The page's custom property `--chip-bg` and the inherited `color` both pass into the shadow root. Ordinary selectors from the page don't.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Default rgb(220, 240, 240) rgb(40, 40, 40)
Themed rgb(255, 228, 196) rgb(40, 40, 40)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A name without a hyphen

In the first example, change both the tag and the define call to userbadge.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "userbadge" is not a valid custom element name

Break #2

Define the same tag twice

Add a second customElements.define("user-badge", class extends HTMLElement {}); at the end of the first example.

terminal
$ Console
── what you'll see ──
Uncaught NotSupportedError: Failed to execute 'define' on 'CustomElementRegistry': the name "user-badge" has already been used with this registry

Myth vs fact

Myth

Web Components replace frameworks like React.

Fact

They solve encapsulation and reuse, not app state or efficient re-rendering. Many teams use them for design-system pieces shared across frameworks, with a framework (or a small helper like Lit) on top.

Myth

Shadow DOM blocks all page styles.

Fact

Inherited properties (color, font) and CSS custom properties pass through by design; only selectors stop at the boundary.

Myth

A closed shadow root keeps its contents secret.

Fact

It only hides element.shadowRoot. Any script on the page can still patch attachShadow before the component runs; it is an API style choice, not security.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Declarative shadow DOM, <template shadowrootmode="open"> inside the host's HTML, lets servers render shadow roots without JavaScript; it reached all major browsers in 2024.

  • ▸

    Avoid heavy work in the constructor: elements created with document.createElement must not get attributes or children there (the spec forbids it and throws). Read attributes in connectedCallback or attributeChangedCallback.

  • ▸

    connectedCallback can run several times (moving an element calls disconnected then connected), so guard one-time setup, as the third example does with if (this.shadowRoot) return.

  • ▸

    Constructable stylesheets (new CSSStyleSheet() + shadowRoot.adoptedStyleSheets = [sheet]) let many component instances share one parsed stylesheet instead of each parsing its own <style>.

Remember this

  1. 1

    Web Components is the name for three browser standards that work together, with no library needed: custom elements (define a new tag and the JavaScript class behind it), shadow DOM (a private, separate DOM tree attached to an element, with its own styles), and **<template> and <slot>** (inert markup to clone, and placeholders for the user's content). They work in every modern browser and with any framework, because to React, Vue or plain HTML they are just elements.

  2. 2

    A custom element is a class that extends HTMLElement, registered with customElements.define("user-badge", UserBadge). The name must contain a hyphen, so it can never clash with a future built-in tag, and each name can be defined only once per page. Elements already in the HTML before the definition are upgraded when it arrives; until then they are "undefined" elements you can style with :not(:defined) to avoid a flash of unstyled content. customElements.whenDefined(name) returns a promise for that moment.

  3. 3

    Lifecycle callbacks are methods the browser calls for you. constructor() runs when the element is created: call super() first, set up state and the shadow root, but don't read attributes or children yet (they may not exist). connectedCallback() runs each time the element is put into the page: render and add listeners here. disconnectedCallback() runs when it's removed: clean up timers and outside listeners. attributeChangedCallback(name, oldValue, newValue) runs when an attribute listed in static observedAttributes changes. On upgrade, the order is constructor, then one attribute callback per existing observed attribute, then connected.

  4. 4

    Shadow DOM: this.attachShadow({ mode: "open" }) creates a shadow root, a hidden mini-document inside the element. Markup inside it is not found by document.querySelector, and its <style> applies only inside, while the page's CSS doesn't reach in (except inherited properties like color and font, and CSS custom properties, which pass through on purpose so you can theme components). That is encapsulation: the component can't break the page and the page can't break the component. :host styles the element itself from inside; ::part(name) lets the page style parts you expose with part="name". mode: "open" exposes element.shadowRoot; "closed" hides it, which is not a security boundary.

  5. 5

    **<template>** holds markup the browser parses but doesn't render or run: images don't load, scripts don't execute. Clone it with template.content.cloneNode(true) to stamp out copies cheaply. A **<slot> inside shadow DOM is a placeholder: the element's own children (the light DOM**) are shown there. <slot name="title"> receives children with slot="title"; an unnamed slot receives the rest. Slotted nodes stay in the light DOM, so the page's CSS still styles them; inside, ::slotted(p) can style them lightly. The slotchange event fires when assigned content changes.

  6. 6

    Components talk to the outside with attributes and properties in, and events out: dispatch a CustomEvent with a detail payload. Events fired inside shadow DOM need composed: true to cross the shadow boundary (and bubbles: true to bubble), and outside listeners see event.target retargeted to the host element, so internals stay hidden. For forms and accessibility, a custom element can join a <form> with static formAssociated = true and attachInternals(), and should carry proper roles, labels and keyboard support (Hectal's /html course covers ARIA). Accessibility is not automatic.

Explain it without notes

01

What are the three parts of Web Components and what does each do?

02

List the custom element lifecycle callbacks and what you should do in each.

03

How do styles work across the shadow boundary?

04

How does a component send data out to the page, and what is event retargeting?

Practice

01

Write a <click-counter> element that shows a button in its shadow DOM and counts clicks, starting from its start attribute (default 0).

02

Make <click-counter> dispatch a composed count-change event with the new count in detail, and log it from the page.

Trade-offs

  • ↔

    Shadow DOM gives true style isolation, but global styles (resets, utility classes, a site theme) no longer reach inside, so theming must be designed through custom properties and parts.

  • ↔

    Web Components work in every framework and need no build step, but you write rendering, state updates and attribute/property syncing yourself, or adopt a small library like Lit.

Done when you can

  • I can define a custom element with a valid hyphenated name.

  • I can use the lifecycle callbacks and observedAttributes correctly.

  • I can attach shadow DOM and explain what does and doesn't cross its boundary.

  • I can build an element from a <template> with named and default slots.

  • I can dispatch a composed CustomEvent and explain retargeting.