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

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:

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

References