Tags: web-dev concept

Web Components

Date: 2026-08-17


The platform’s own component model: custom elements, shadow DOM and slots. The encapsulation is genuine in a way no framework’s is — styles cannot leak in or out — and that same strictness is why they’re used far more in design systems shipped across teams than inside single applications.


Web components are reusable elements built from browser standards alone — a custom element class registered under a hyphenated tag name, optionally with a shadow DOM (Document Object Model) and slots — so they work in any framework or none.

The three pieces

They’re separable, and using one without the others is normal.

CUSTOM ELEMENTS   define a new tag with lifecycle callbacks
SHADOW DOM        a subtree with its own scope for styles and IDs
SLOTS             holes in that subtree for the consumer's content

The smallest working form

class PriceTag extends HTMLElement {
  connectedCallback() {                    // fires when added to the document
    this.textContent = `£${this.getAttribute('amount')}`;
  }
}
customElements.define('price-tag', PriceTag);
<price-tag amount="24.99"></price-tag>

That’s complete. No shadow DOM, no build step, no framework. The name must contain a hyphen — that’s the rule that guarantees it never collides with a future HTML element.

Adding encapsulation and a slot:

class ProductCard extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = `
      <style>
        /* scoped. cannot affect anything outside this component,
           and outside CSS cannot reach in */
        .card { border: 1px solid var(--card-border, #ddd); padding: 1rem; }
        h3 { margin: 0 0 .5rem; font-size: 1.1rem; }
      </style>
      <div class="card">
        <h3><slot name="title">Untitled</slot></h3>
        <slot></slot>                          <!-- default slot -->
      </div>
    `;
  }
}
customElements.define('product-card', ProductCard);
<product-card>
  <span slot="title">Navy overcoat</span>
  <p>Wool blend, made in Portugal.</p>
</product-card>

The lifecycle

constructor()              element created. DO NOT touch attributes or
                           children here — they may not exist yet

connectedCallback()        added to the document. ← do setup here
                           can fire MORE THAN ONCE if the element moves

disconnectedCallback()     removed. ← clean up listeners and timers here,
                           or you leak — Memory and Long Sessions

attributeChangedCallback   an observed attribute changed
  (name, old, new)         requires: static observedAttributes = ['amount']

adoptedCallback()          moved to a new document. rare

connectedCallback firing more than once is the trap. Moving an element in the DOM disconnects and reconnects it, so setup that assumes it runs once will run twice.

The cleanup obligation in disconnectedCallback is not optional — an element that adds listeners and never removes them retains its whole subtree — Memory and Long Sessions.

What the encapsulation actually costs

Shadow DOM’s isolation is total, and that cuts both ways.

STYLES DON'T LEAK OUT           ✓ the point

OUTSIDE STYLES DON'T REACH IN   ✗ your design system's global styles
                                  don't apply either. no utility classes,
                                  no reset, no inherited component CSS

  the sanctioned ways through:
    CSS custom properties     --card-border, as above   ← the main one
    ::part(name)              expose specific internals for styling
    :host / :host-context     style the element from inside
    inherited properties      font, colour still inherit

Custom properties are the styling API of a web component, and designing that surface deliberately is most of the work of making one reusable — Custom Properties, Design Tokens, Theming.

Other real costs:

  • Forms don’t work by default. An <input> inside shadow DOM isn’t associated with an outer <form> and won’t be submitted with it. Form-associated custom elements fix this and are more work than expected — Forms
  • Accessibility across the boundary is awkward. aria-labelledby referencing an ID cannot cross into or out of a shadow root, because IDs are scoped — ARIA, The Accessibility Tree
  • Server rendering is limited. Declarative shadow DOM exists but framework support is uneven, so a component-heavy page may render nothing meaningful until JavaScript runs — Rendering and SEO, Progressive Enhancement
  • Global search and third-party tooling — analytics selectors, session replay and testing tools may not see into shadow roots without configuration — Session Replay

Where they genuinely win

Design systems consumed by teams on different stacks. One button component that works identically in a React storefront, a Vue admin panel and a plain-HTML email preview tool is something no framework component can be — and it’s the reason large organisations reach for them.

FRAMEWORK COMPONENT              WEB COMPONENT

one framework, one version       any framework, or none
rich ergonomics, typed props     attributes are strings; objects need
  and rich composition             properties set in JS
fast iteration                   slower, more ceremony
dies with the framework          outlives it

Also good for third-party embeds — a widget dropped into someone else’s page where style collisions would otherwise be certain.

Inside one application on one framework, they’re usually the wrong choice. The framework’s own components are better integrated, better typed and less ceremony, and the isolation you’re paying for solves a problem you don’t have — Headless Components, Component API Design.

Where it interacts

  • The DOM — shadow DOM is a real part of the tree with its own scoping rules
  • Design Systems — the cross-stack distribution problem that makes these worth the cost
  • ARIA — the boundary complicates every ID-based accessibility relationship
  • Progressive Enhancement — an undefined custom element renders its children as plain HTML, which is a usable baseline if you design for it