Component & UI

Web Components

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your w

Three Core Technologies

Web Components consists of three main technologies, which can be used together to create versatile custom elements with encapsulated functionality that can be reused wherever you like without fear of code collisions.

Custom Elements

Define and register custom HTML elements with JavaScript

A set of JavaScript APIs that allow you to define custom elements and their behavior, which can then be used as desired in your user interface. Custom element names must contain a hyphen (e.g., my-element).

my-element user-card app-header
myelement myElement header

Custom element names must contain a hyphen (-). This prevents name collisions with native HTML elements.

Basic usage
class MyElement extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<p>Hello, Web Components!</p>';
  }
}
customElements.define('my-element', MyElement);
<!-- Use in HTML -->
<my-element></my-element>
Two Types of Custom Elements
Autonomous
<my-element>

Extends HTMLElement. Defines a completely new element.

Customized Built-in
<p is="my-p">

Extends existing HTML elements. Inherits accessibility automatically.

Lifecycle Callbacks

Custom elements have four lifecycle callbacks managed by the browser. These callbacks let you define behavior in response to element creation, DOM connection, attribute changes, and DOM disconnection.

constructor() Element created
connectedCallback() Added to DOM
attributeChangedCallback() Attribute changed
disconnectedCallback() Removed from DOM
1
constructor() When element is created

Set up initial state. Must call super(). Create Shadow DOM here.

✓ DO
  • super() call
  • attachShadow()
  • Initialize state
✗ DON'T
  • Read attributes
  • Access children
  • innerHTML assignment
2
connectedCallback() When added to DOM

Called when element is connected to the DOM tree. Render content, add event listeners, and fetch data here.

connectedCallback() {
  this.shadowRoot.innerHTML = `
    <style>:host { display: block; }</style>
    <h2>${this.getAttribute('title')}</h2>
    <slot></slot>
  `;
  this.addEventListener('click', this._onClick);
}
3
attributeChangedCallback(name, oldVal, newVal) When an observed attribute changes

Called when attributes listed in static observedAttributes are added, removed, or changed. Enables reactive UI updates.

static observedAttributes = ['name', 'theme'];

attributeChangedCallback(name, oldVal, newVal) {
  switch (name) {
    case 'name':
      this.shadowRoot.querySelector('.name').textContent = newVal;
      break;
    case 'theme':
      this.shadowRoot.host.classList.toggle('dark', newVal === 'dark');
      break;
  }
}
4
disconnectedCallback() When removed from DOM

Called when element is disconnected from DOM. Remove event listeners, clear timers, release external resources. Critical for preventing memory leaks.

disconnectedCallback() {
  this.removeEventListener('click', this._onClick);
  clearInterval(this._timer);
  this._observer?.disconnect();
}
Observing attribute changes
class UserCard extends HTMLElement {
  static observedAttributes = ['name', 'avatar'];

  attributeChangedCallback(attr, oldVal, newVal) {
    if (attr === 'name') {
      this.shadowRoot.querySelector('.name').textContent = newVal;
    }
  }

  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        :host { display: block; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
        .name { font-weight: bold; font-size: 1.2rem; }
      </style>
      <div class="name">${this.getAttribute('name') ?? ''}</div>
      <slot></slot>
    `;
  }
}
customElements.define('user-card', UserCard);
<user-card name="Alice">Frontend Developer</user-card>

Shadow DOM

Encapsulate component internals and styles from the outside

A set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality. This way, you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.

DOM Structure Comparison
Normal DOM
<div>
<style> ⚠ Global
<p>
Shadow DOM
<my-element>
#shadow-root
<style> ✓ Scoped
<p>
<slot>
Shadow DOM Modes
mode: 'open'
element.shadowRoot

External JS can access shadowRoot. The common choice.

mode: 'closed'
element.shadowRoot → null

Not accessible externally. Note: this is NOT a security boundary.

Creating Shadow DOM
class MyCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        /* This style does not affect the outside */
        :host { display: block; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
        p { color: blue; font-weight: bold; }
      </style>
      <p><slot></slot></p>
    `;
  }
}
customElements.define('my-card', MyCard);
Shadow DOM CSS Selectors
:host Style the shadow host itself
:host(.active) Conditionally style the host
::slotted(span) Style slotted elements
::part(header) Style shadow parts from outside
CSS Custom Properties penetrate Shadow DOM

Normal CSS doesn't affect Shadow DOM internals, but CSS custom properties (variables) are the exception. A --my-color defined outside can be used as var(--my-color) inside Shadow DOM. This is the foundation for theming Web Components.

:root {
  --card-bg: #f8fafc;    /* Defined outside */
  --card-text: #1e293b;
}

/* Accessible inside Shadow DOM */
:host {
  background: var(--card-bg);
  color: var(--card-text);
}

HTML Templates

Define non-rendered markup and reuse it with JavaScript

The <template> and <slot> elements enable you to write markup templates that are not displayed in the rendered page. These can then be reused multiple times as the basis of a custom element's structure, with slots allowing flexible content insertion from outside.

Template Flow
1
Define
<template>

Written in HTML but not rendered

2
Clone
.cloneNode(true)

Clone template with JS

3
Insert
<slot>

Append to Shadow DOM and render

Types of Slots
Default Slot
<slot>

Unnamed. Receives all children without a slot attribute.

Named Slot
<slot name="header">

Receives only elements with the matching slot attribute.

Using Templates and Slots
<!-- Template definition -->
<template id="card-template">
  <style>
    .card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; }
    .card-header { font-weight: bold; margin-bottom: 0.5rem; }
    .card-footer { margin-top: 0.5rem; font-size: 0.85rem; color: #666; }
  </style>
  <div class="card">
    <div class="card-header"><slot name="title">Default Title</slot></div>
    <div><slot>Default content</slot></div>
    <div class="card-footer"><slot name="footer"></slot></div>
  </div>
</template>
<!-- Usage: place content in multiple slots -->
<my-card>
  <span slot="title">Web Components Guide</span>
  <p>Learn about Custom Elements, Shadow DOM, and Templates.</p>
  <span slot="footer">Last updated: 2026</span>
</my-card>
Using Templates with JavaScript
class MyCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('card-template');
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

Putting It All Together

Combining Custom Elements, Shadow DOM, and HTML Templates creates a reusable, framework-independent component. Below is a profile card implemented as a Web Component.

Custom Elements
Class definition + lifecycle
+
Shadow DOM
Style isolation + encapsulation
+
HTML Templates
Template reuse + slots
=
Result
<profile-card>

Web Components Demo: <profile-card>

Edit the code and press "Run" to see changes in the preview. Try changing slot content or the theme attribute.

PreviewFullscreen
💡
Key Point

This <profile-card> works with any framework — React, Vue, Svelte, or vanilla HTML. Framework independence makes Web Components ideal for shared design system components.

Browser Support

Feature Desktop Mobile
Chrome
Edge
Firefox
Safari
Chrome Android
Safari iOS
26
13
22
8
26
8
DOM API

The CustomElementRegistry interface provides methods for registering custom elements and querying registered elements. To get an instance of it, use the window.customElements property. To create a scoped registry, use the CustomElementRegistry.CustomElementRegistry() constructor.

54
79
63
10.1
54
10.3

The define() method of the CustomElementRegistry interface adds a definition for a custom element to the custom element registry, mapping its name to the constructor which will be used to create it.

54
79
63
10.1
54
10.3
define (disabledFeatures static property)

Supports `disabledFeatures` static property

77
79
92
77

The get() method of the CustomElementRegistry interface returns the constructor for a previously-defined custom element.

54
79
63
10.1
54
10.3

The getName() method of the CustomElementRegistry interface returns the name for a previously-defined custom element.

117
117
116
17
117
17

The upgrade() method of the CustomElementRegistry interface upgrades all shadow-containing custom elements in a Node subtree, even before they are connected to the main document.

68
79
63
12.1
68
12.2

The whenDefined() method of the CustomElementRegistry interface returns a Promise that resolves when the named element is defined.

54
79
63
10.1
54
10.3

The Element.attachShadow() method attaches a shadow DOM tree to the specified element and returns a reference to its ShadowRoot.

53
79
63
10
53
10

A boolean that specifies whether the shadow root is clonable: when set to true, the shadow host cloned with Node.cloneNode() or Document.importNode() will include shadow root in the copy. Its default value is false.

124
124
123
17.4
124
17.4

A boolean that, when set to true, specifies behavior that mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling. Its default value is false.

53
79
94
13.1
53
13.4

A boolean that, when set to true, indicates that the shadow root is serializable. If set, the shadow root may be serialized by calling the Element.getHTML() or ShadowRoot.getHTML() methods with the options.serializableShadowRoots parameter set true. Its default value is false.

125
125
128
18
125
18

The Element.shadowRoot read-only property represents the shadow root hosted by the element.

35
79
63
10
35
10

The read-only composed property of the Event interface returns a boolean value which indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM.

53
79
52
10
53
10

The composedPath() method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked. This does not include nodes in shadow trees if the shadow root was created with its ShadowRoot.mode closed.

53
79
59
10
53
10

The HTMLTemplateElement interface enables access to the contents of an HTML template element.

26
13
22
8
26
8

The content property of the HTMLTemplateElement interface returns the element's template contents as a DocumentFragment. This content's Node/ownerDocument is a separate Document from the one that contains the element itself — unless the containing document is itself constructed for the purpose of holding template content.

26
13
22
8
26
8

The getRootNode() method of the Node interface returns the context object's root, which optionally includes the shadow root if it is available.

54
79
53
10.1
54
10.3

The read-only isConnected property of the Node interface returns a boolean indicating whether the node is connected (directly or indirectly) to a Document object.

51
79
49
10
51
10

The ShadowRoot interface of the Shadow DOM API is the root node of a DOM subtree that is rendered separately from a document's main DOM tree.

53
79
63
10
53
10

The clonable read-only property of the ShadowRoot interface returns true if the shadow root is clonable, and false otherwise.

124
124
123
17.4
124
17.4

The delegatesFocus read-only property of the ShadowRoot interface returns true if the shadow root delegates focus, and false otherwise.

53
79
94
15
53
15

The host read-only property of the ShadowRoot returns a reference to the DOM element the ShadowRoot is attached to.

53
79
63
10
53
10

The mode read-only property of the ShadowRoot specifies its mode — either open or closed. This defines whether or not the shadow root's internal features are accessible from JavaScript.

53
79
63
10.1
53
10.3

The serializable read-only property of the ShadowRoot interface returns true if the shadow root is serializable.

125
125
128
18
125
18

The customElements read-only property of the Window interface returns a reference to the global CustomElementRegistry object, which can be used to register new custom elements and get information about previously registered custom elements.

54
79
63
10.1
54
10.3
Other

`:defined`

54
79
63
10
54
10
1+Supported (version) Not supported Has note Sub-feature descriptions sourced from MDN Web Docs (CC BY-SA 2.5)
Notes 2 item(s)
Limitation
  • This browser only partially implements this feature
Implementation note
  • Supports 'Autonomous custom elements' but not 'Customized built-in elements'. See bug 182671.
Notes 2 item(s)
Limitation
  • This browser only partially implements this feature
Implementation note
  • Supports 'Autonomous custom elements' but not 'Customized built-in elements'. See bug 182671.
Notes 2 item(s)
Limitation
  • This browser only partially implements this feature
Implementation note
  • Supports 'Autonomous custom elements' but not 'Customized built-in elements'.
Notes 2 item(s)
Limitation
  • This browser only partially implements this feature
Implementation note
  • Supports 'Autonomous custom elements' but not 'Customized built-in elements'.
Notes 1 item(s)
Implementation note
  • Previously available under a different name: cloneable (16.4)
Notes 1 item(s)
Implementation note
  • Previously available under a different name: cloneable (16.4)
Notes 1 item(s)
Implementation note
  • Before Firefox 95, this property was incorrectly set to `false` on `<select>` and `<input type='checkbox'>` elements.
Notes 2 item(s)
Removed
  • This feature was removed in a later browser version (53)
Implementation note
  • Previously available under a different name: deepPath (50)
Notes 2 item(s)
Removed
  • This feature was removed in a later browser version (53)
Implementation note
  • Previously available under a different name: deepPath (50)

Accessibility Considerations

Shadow DOM content is accessible to screen readers, but custom element names alone don't convey meaning. You must use semantic HTML elements inside and set appropriate ARIA attributes.

✗ Incorrect
<!-- Custom element name alone is not meaningful -->
<my-button>Submit</my-button>

Screen readers don't know how to handle <my-button>. It won't be recognized as a button and won't be keyboard-operable.

✓ Correct
<!-- Place semantic elements inside -->
<my-button>
  #shadow-root
    <button role="button" aria-label="Submit">
      <slot></slot>
    </button>
</my-button>

Using <button> inside enables screen reader recognition, focus management, and keyboard operability automatically.

Checklist
  • Use native elements like <button>, <a>, <input> for interactive elements
  • Set appropriate role attributes on custom elements (role="tablist", role="dialog", etc.)
  • Provide context with aria-label / aria-describedby
  • Implement keyboard interactions (Tab / Enter / Space / Escape)
  • Focus management: consider delegatesFocus for focusable elements inside Shadow DOM

Security Considerations

Shadow DOM is not a security boundary

Shadow DOM provides style encapsulation, but JavaScript can access it via element.shadowRoot. It cannot be used to hide sensitive data.

Risks of direct innerHTML insertion

Directly setting user input to innerHTML inside Custom Elements creates XSS vulnerabilities. Use textContent or DOM APIs for safe content insertion.