Web Components
Web Components are the browser's native component model: a set of standards (custom elements, Shadow DOM, templates) that let you define your own HTML tags — <user-card>, <date-picker> — with encapsulated markup, styles, and behavior, no framework required.
Their superpower is longevity and neutrality: a Web Component works in plain HTML, inside React, Vue, Svelte, a CMS, or a page that outlives three framework rewrites. That's why design systems at Adobe (Spectrum), Microsoft (FluentUI), SAP, and GitHub ship as Web Components — write the button once, every team consumes it regardless of stack.
TL;DR
- Three standards compose the model: Custom Elements (define
<my-tag>with lifecycle callbacks), Shadow DOM (encapsulated DOM + scoped CSS), and templates/slots (inert markup + content projection). - Shadow DOM gives real style isolation — page CSS can't break your component, your CSS can't leak out (themable via CSS custom properties and
::part). - Raw APIs are verbose; in practice you write components with Lit (~5 KB) — reactive properties, declarative templates, same standards underneath.
- Custom element names must contain a hyphen (
user-card, neverusercard). - Best fit: design systems, embeddable widgets, and cross-framework/CMS component sharing. For building a whole app, frameworks remain more productive.
- Attributes carry strings; properties carry real data — the attribute/property distinction is the top source of confusion.
Quick Example
A complete, dependency-free component:
This tag now works in any HTML page and inside any framework's templates.
Core Concepts
Custom Elements
A class extending HTMLElement, registered with a hyphenated tag name, with lifecycle callbacks:
Shadow DOM
attachShadow() gives the element a private subtree:
- Style encapsulation both ways — the reason design systems love it. No BEM, no CSS-modules build step, no specificity wars with host-page CSS.
- Composition via
<slot>— light-DOM children project into named or default slots, like frameworkchildren/slots. - Deliberate theming seams — inheritable properties (fonts,
color) and CSS custom properties pierce the boundary by design;::part()exposes specific internals for external styling:
Attributes vs Properties
The distinction that bites everyone: HTML attributes are always strings; JS properties hold real values.
Well-built components reflect simple attributes to properties and accept rich data only via properties. Frameworks differ in which they set — the historical source of React's Web Component friction (React 19 finally sets properties intelligently).
Events Out
Components communicate upward with standard DOM events:
Any consumer — vanilla, React, Vue — uses addEventListener("item-selected", ...).
Lit: How Components Are Actually Written
The raw APIs work but are boilerplate-heavy (manual DOM updates, attribute parsing). Lit is the de facto authoring library — tiny, standards-based, with reactive properties and tagged-template rendering:
Property changes re-render efficiently (only dynamic parts update). Alternatives: Stencil (compiler, generates framework wrappers — powers Ionic), Fast, or compiling Svelte to custom elements.
Web Components vs Framework Components
The pragmatic reading: these are complementary layers. Teams build applications in a framework and increasingly ship their design system as Web Components underneath — the button outlives the framework choice. For a single-stack product team, framework components alone remain the productive default.
Common Mistakes
Passing Complex Data Through Attributes
JSON.stringify into an attribute works and is wrong. Use properties for objects/arrays; reserve attributes for strings, numbers, and boolean flags.
Doing Work in the Constructor
Attributes aren't set and children don't exist yet. Fetching, measuring, and listener setup belong in connectedCallback (with cleanup in disconnectedCallback — components get moved and reattached).
Fighting Shadow DOM Instead of Designing Seams
If consumers need to restyle everything, Shadow DOM feels hostile. Design the theming API up front: CSS custom properties for tokens, part for structural overrides, slots for content. (For form controls, use ElementInternals so inputs inside shadow roots participate in native forms.)
Forgetting composed: true
A CustomEvent without it stops at the shadow boundary and the parent never hears it — a classic silent bug.
Ignoring SSR Until It Hurts
Client-only custom elements flash empty on first paint. Declarative Shadow DOM (<template shadowrootmode="open">) enables server rendering — Lit ships SSR support — but it needs to be in the plan, not an afterthought.
FAQ
Are Web Components "done" and safe to use?
Yes — custom elements and Shadow DOM have shipped in every major browser for years, and the last gaps (declarative Shadow DOM for SSR, React interop) closed. GitHub's UI, Adobe's and Microsoft's design systems, and YouTube run on them in production.
Do Web Components replace React?
No — they standardize the component boundary, not application architecture (state, routing, data). The realistic pattern is coexistence: framework for the app, Web Components for the shared/embeddable layer.
How do they work inside React?
React 19 handles custom elements properly (props → properties, events attachable). Pre-19 required refs for properties and manual addEventListener — the historical pain point. Vue, Svelte, and Angular have long supported them cleanly.
What about accessibility?
Same rules as any DOM (Accessibility) — Shadow DOM doesn't hide content from screen readers. Care points: ARIA references can't cross shadow boundaries, and form controls should use ElementInternals for native form/label participation.
When should I choose them?
Strong signals: multiple frameworks/CMSes must consume the same UI; you ship an embeddable widget onto pages you don't control; you're building a design system meant to outlast framework churn. Absent those, your framework's components are the simpler path.
Related Topics
- HTML — The platform being extended
- React — The framework counterpart (and interop story)
- CSS — Custom properties,
::part, and scoping - Svelte — Compiles to custom elements
- Accessibility — ARIA across shadow boundaries
- State Management — What Web Components don't give you