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.
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.
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.
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.
<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
Watch disconnect
In the first example, add a
disconnectedCallback() { console.log("disconnected"); }method andbadge.remove();at the end. Predict where the new line appears in the console. - 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
Make it closed
In the first example, change
mode: "open"to"closed". Predict: what isbadge.shadowRootnow, and which line breaks? (Keep a reference torootin the class to fix it.)
Code & diagrams
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
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, RaviThe 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
Console
Nothing logged yet.Expected console output
title [ <span> ]
[ <p> ]
closed: button <info-card>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
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.
Break #2
Define the same tag twice
Add a second customElements.define("user-badge", class extends HTMLElement {}); at the end of the first example.
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.createElementmust not get attributes or children there (the spec forbids it and throws). Read attributes inconnectedCallbackorattributeChangedCallback. - ▸
connectedCallbackcan run several times (moving an element calls disconnected then connected), so guard one-time setup, as the third example does withif (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
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
A custom element is a class that
extends HTMLElement, registered withcustomElements.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
Lifecycle callbacks are methods the browser calls for you.
constructor()runs when the element is created: callsuper()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 instatic observedAttributeschanges. On upgrade, the order is constructor, then one attribute callback per existing observed attribute, then connected. - 4
Shadow DOM:
this.attachShadow({ mode: "open" })creates a shadow root, a hidden mini-document inside the element. Markup inside it is not found bydocument.querySelector, and its<style>applies only inside, while the page's CSS doesn't reach in (except inherited properties likecolorandfont, 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.:hoststyles the element itself from inside;::part(name)lets the page style parts you expose withpart="name".mode: "open"exposeselement.shadowRoot;"closed"hides it, which is not a security boundary. - 5
**
<template>** holds markup the browser parses but doesn't render or run: images don't load, scripts don't execute. Clone it withtemplate.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 withslot="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. Theslotchangeevent fires when assigned content changes. - 6
Components talk to the outside with attributes and properties in, and events out: dispatch a
CustomEventwith adetailpayload. Events fired inside shadow DOM needcomposed: trueto cross the shadow boundary (andbubbles: trueto bubble), and outside listeners seeevent.targetretargeted to the host element, so internals stay hidden. For forms and accessibility, a custom element can join a<form>withstatic formAssociated = trueandattachInternals(), and should carry proper roles, labels and keyboard support (Hectal's/htmlcourse covers ARIA). Accessibility is not automatic.
Explain it without notes
What are the three parts of Web Components and what does each do?
List the custom element lifecycle callbacks and what you should do in each.
How do styles work across the shadow boundary?
How does a component send data out to the page, and what is event retargeting?
Practice
Write a <click-counter> element that shows a button in its shadow DOM and counts clicks, starting from its start attribute (default 0).
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
observedAttributescorrectly.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
CustomEventand explain retargeting.