Real World Web Components
Accessibility challenges across the shadow boundary, Declarative Shadow DOM for SSR without JavaScript, SEO indexing behavior, and CSS custom property naming conventions.
I ran into the state management gaps early and expected them. What caught me off guard were the quieter rough edges -- the ones that don't get nearly as much discussion.
Accessibility across the shadow boundary, server-side rendering, search indexing, CSS variable naming. All have workable answers. Each one needs a mental adjustment.
Accessibility and the Shadow Boundary
Most accessibility patterns work fine inside a shadow root. Roles, keyboard interactions, ARIA on internal elements -- all fine. The difficult case is label association.
Standard HTML form pattern:
<label for="name-input">Full name</label>
<input id="name-input" type="text">When the <input> lives inside a shadow root, the for attribute cannot reach it. The label can only reference elements in the same DOM tree. The input is invisible to it from the outside.
The delegatesFocus option partially addresses keyboard navigation:
this.attachShadow({ mode: 'open', delegatesFocus: true });When the host element receives focus, the browser delegates it to the first focusable element inside the shadow root. This helps tab order but does not fix label association.
Full label-to-shadow-input association is being addressed through the Accessibility Object Model (AOM) -- explicit ARIA relationship APIs that can cross the shadow boundary. Until that lands broadly, custom form elements need aria-label on the host and careful screen reader testing. Do not assume accessible HTML inside a shadow root equals an accessible component.
Focus Locking
Modals and dialogs need to trap focus while open. Inside a shadow root, the trap must account for the boundary.
The inert attribute is the modern answer:
<main inert><!-- app content, inaccessible while modal is open --></main>
<my-dialog><!-- modal content, the only focusable region --></my-dialog>Setting inert on the rest of the page removes it from the accessibility tree and blocks focus. When the modal closes, remove the attribute and focus returns to where it was.
inert works at the DOM level, not the shadow level. It does not care about shadow roots. Chrome, Safari, and Firefox all ship it natively -- a polyfill exists for older environments.
Declarative Shadow DOM
Server-side rendering web components has historically required JavaScript. The browser needs attachShadow() to create the shadow root, and that is a JavaScript call.
Declarative Shadow DOM removes that requirement:
<task-card>
<template shadowrootmode="open">
<style>:host { display: block; padding: 1rem; }</style>
<slot></slot>
</template>
Buy oat milk
</task-card>No JavaScript. The browser parses shadowrootmode="open" and creates the shadow root immediately -- on the HTML parse pass, before any scripts run. When the component's JavaScript eventually loads, it either adopts or replaces the existing template.
The content is visible on first paint. A server template -- PHP, Next.js, Astro, a static site generator -- can render the full initial HTML including the shadow structure. The component becomes interactive when JavaScript loads, but it is already rendered.
This is the SSR answer for web components. The initial paint does not depend on JavaScript.
ExpandStandard web component SSR vs Declarative Shadow DOM: first-paint comparison
SEO
Googlebot can index web components. The crawler flattens shadow DOM and light DOM together for the purposes of indexing. Content inside a shadow root is not hidden from Google.
Bing and DuckDuckGo are less consistent -- there are documented cases where shadow root content was missed during crawl. The practical guideline: put important text in the light DOM (slotted content or outside the component) rather than deep inside the shadow tree. Light DOM content is indexed by all crawlers without exception.
Do not use shadow DOM to hide text you want search engines to find. The shadow boundary is a styling and scripting boundary, not a visibility boundary.
CSS Variable Naming Conventions
When you expose CSS custom properties as your component's styling API, naming them with the component tag as a prefix prevents collisions:
:host {
background: var(--task-card-bg, #ffffff);
border-radius: var(--task-card-radius, 8px);
padding: var(--task-card-padding, 1rem);
}A consumer sets them explicitly:
task-card {
--task-card-bg: #f5f5f5;
--task-card-radius: 4px;
}The prefix scopes the contract. If the page already uses --bg as a global variable and your component picks it up, that is an unintended side effect. Prefixing by element name makes it clear these properties belong to this component.
Whether you define the defaults on :host or :root does not matter functionally -- CSS custom properties cascade either way. :host is slightly more intentional: it says "these belong to this component." Pick one and be consistent.
Web Components as Platform Blueprints
One idea worth sitting with: web components are not just a component library strategy -- they are informing how native HTML elements get designed.
Open UI, the W3C community group working on new HTML elements, is using the part attribute in proposals for native <select>, picker, and tab-like elements. The ::part() pseudo-element syntax that lets you style named shadow parts is showing up in specs for elements that do not use shadow DOM at all.
The patterns you learn writing web components are patterns the web platform is converging on. A solid tab component you write today is a step toward understanding what a native <tabs> element will look like when it ships. The web component community is not just consuming the platform -- it is shaping it.
The Essentials
- Label association across the shadow boundary does not work with
for. Usearia-labelon the host and test with a screen reader. delegatesFocus: trueroutes focus inward on tab but does not fix label association.- The
inertattribute is the modern focus-locking tool. It removes content from the accessibility tree at the DOM level -- shadow roots are not an obstacle. - Declarative Shadow DOM (
shadowrootmode="open") renders a shadow tree as HTML with no JavaScript required. The browser creates the shadow root on first parse. - Googlebot indexes shadow DOM. Bing and DuckDuckGo are less consistent -- keep important text in the light DOM.
- Prefix CSS custom properties with the component tag name.
--task-card-bgbeats--bgfor avoiding cascade collisions.
Further Reading and Watching
- Declarative Shadow DOM: Hello HTML -- step-by-step introduction to writing shadow roots directly in HTML without JavaScript
- Declarative Shadow DOM -- web.dev -- reference for the
shadowrootmodeattribute, streaming HTML parsing, and the server-side rendering pattern
Keep reading