Web Components & Framework-Agnostic Grids
Permalink to "Web Components & Framework-Agnostic Grids"Design systems serving several frameworks increasingly ship web components, and teams using compiler-based frameworks like Svelte often build their own tables. Both approaches can produce excellent accessibility — web components render real HTML, and Svelte compiles to plain DOM with compile-time accessibility checks. But web components introduce one structural complication data UIs feel acutely: shadow DOM boundaries. ID references cannot cross them, table structure cannot be assembled across them, and semantics on a custom element’s host need deliberate handling.
This topic explains the boundary rules and the patterns that work: keep relationships inside one tree, render whole tables in one shadow root (or enhance light-DOM tables), set default semantics with ElementInternals, and use slots only for content that is not table structure. The users most affected are screen reader users, for whom broken references mean unnamed grids and silent comboboxes, and broken structure means tables read as fragments. The failure this topic prevents is a design system component that looks perfect in every framework and is unusable with a screen reader in all of them.
This topic belongs to accessible data component libraries.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | Web component risk |
|---|---|---|
| 1.3.1 Info and Relationships | A | Table structure split across roots; slotted rows |
| 4.1.2 Name, Role, Value | A | Cross-root aria-labelledby; host semantics |
| 4.1.3 Status Messages | AA | Live regions inside components vs host app |
| 2.1.1 Keyboard | A | Focus delegation into shadow roots |
| 2.4.7 Focus Visible | AA | Focus styles across the boundary (parts, custom properties) |
Prerequisites
Permalink to "Prerequisites"- Semantic HTML table construction — the structure to preserve.
- Accessible names & descriptions for data widgets — the relationships at risk.
- Composite widget roles & states — roles for custom elements.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"| Mechanism | Works across shadow boundary? | Use for |
|---|---|---|
ID references (aria-labelledby etc.) |
No | Relationships inside one root |
Element reflection (ariaLabelledByElements) |
Limited | Some ancestor references, where supported |
ElementInternals ARIA |
— (host only) | Default role and states for custom elements |
| Slots | Content flattens | Captions, toolbars, empty states |
Table structure via slotted tr/td |
Unreliable | Avoid |
delegatesFocus |
Yes | Focusing the first focusable inside |
::part, custom properties |
Yes | Theming focus and selection |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Map relationships
Permalink to "Step 1 — Map relationships"List every IDREF the component needs; redesign so both ends are in one root. See Shadow DOM and cross-root ARIA references.
Step 2 — Choose an architecture
Permalink to "Step 2 — Choose an architecture"Data-driven shadow component, or light-DOM enhancement of an author’s table. See slotted content and table semantics.
Step 3 — Default semantics
Permalink to "Step 3 — Default semantics"this.#internals = this.attachInternals(); this.#internals.role = 'gridcell';
See ElementInternals for custom grid elements.
Step 4 — Build the table
Permalink to "Step 4 — Build the table"html`<table><caption>${this.label}</caption>…</table><p role="status">${this.status}</p>`
See building an accessible data table in Lit.
Step 5 — Compiler frameworks
Permalink to "Step 5 — Compiler frameworks"{#each sorted as row (row.id)}<tr>…</tr>{/each}
See accessible data tables in Svelte.
Step 6 — Test in three engines
Permalink to "Step 6 — Test in three engines"Read the accessibility tree in Chrome, Firefox and Safari; boundary behaviour differs.
Keyboard interaction contract
Permalink to "Keyboard interaction contract"| Context | Key | Expected | Boundary consideration |
|---|---|---|---|
| Host focus | Tab | Focus enters the table or grid | delegatesFocus or inner tabindex |
| Header button | Enter | Sort and announce | Status inside the root |
| Grid cells | Arrows | Roving focus | Managed inside one root |
| Slotted toolbar | Tab | Toolbar controls before the table | Light DOM order |
| Escape in slotted popover | Escape | Closes, focus back to trigger | Trigger may be in another root |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"| Scenario | Chrome | Firefox | Safari | Notes |
|---|---|---|---|---|
| Native table in one shadow root | Works | Works | Works | Recommended |
| Caption from slotted text | Works | Works | Works | Flattened |
| aria-labelledby to page heading | Fails | Fails | Fails | Use label property |
| Slotted tr into shadow table | Unreliable | Unreliable | Unreliable | Avoid |
| ElementInternals role | Works | Works (recent) | Works (recent) | Fallback for old versions |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. Unnamed grids
Permalink to "1. Unnamed grids"Diagnosis: aria-labelledby crosses the boundary. Fix: label property and internal caption.
2. Fragmented tables
Permalink to "2. Fragmented tables"Diagnosis: rows slotted or wrapped in separate shadow roots. Fix: data-driven rows in one root.
3. Comboboxes split across components
Permalink to "3. Comboboxes split across components"Diagnosis: aria-activedescendant to a listbox in another root. Fix: keep input and listbox together.
4. Host attributes that do nothing
Permalink to "4. Host attributes that do nothing"Diagnosis: aria-label on the host of a component whose inner grid needs it. Fix: map documented attributes to inner elements.
5. Untestable closed roots
Permalink to "5. Untestable closed roots"Diagnosis: tools cannot inspect. Fix: open roots for accessible components.
Shadow DOM and cross-root ARIA references
Permalink to "Shadow DOM and cross-root ARIA references"ID references do not cross shadow boundaries; keep both ends in one tree, slot page content in, or accept strings. See Shadow DOM and cross-root ARIA references.
<data-grid label="Invoices"></data-grid>
ElementInternals for custom grid elements
Permalink to "ElementInternals for custom grid elements"Default roles and states without attribute sprouting; authors can override. See ElementInternals for custom grid elements.
this.#internals.ariaSelected = 'true';
Building an accessible data table in Lit
Permalink to "Building an accessible data table in Lit"A complete native table in one shadow root, caption from a property, sort buttons with aria-sort and an internal status region. See building an accessible data table in Lit.
<th scope="col" aria-sort=${dir}><button>…</button></th>
Accessible data tables in Svelte
Permalink to "Accessible data tables in Svelte"Semantic markup, keyed each blocks, a persistent status region and compile-time warnings treated as errors. See accessible data tables in Svelte.
<p role="status" class="visually-hidden">{status}</p>
Slotted content and table semantics
Permalink to "Slotted content and table semantics"Slot captions, toolbars and empty states; pass rows as data; enhance light-DOM tables when authors own markup. See slotted content and table semantics.
<sortable-table><table>…</table></sortable-table>
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Host app integration. Components with internal status regions should let host apps take over announcements (an announce event or a property to disable internal messages) so apps with a central announcer avoid collisions.
Theming focus. Expose ::part() names and custom properties for focus rings and selection, and document the required contrast; otherwise product themes lose focus visibility.
Server rendering. Declarative shadow DOM lets shadow components render on the server; ensure the initial HTML includes the complete table and caption.
Framework wrappers. React and other wrappers should pass properties (not only attributes) and map documented aria-* props to the component’s label properties.
Worked example: a design system serving four frameworks
Permalink to "Worked example: a design system serving four frameworks"A company’s design system shipped a <ds-data-table> web component used by React, Angular, Vue and plain server-rendered apps. Its first version had rows as nested custom elements and aria-labelledby pointing at page headings; screen readers found unnamed tables read as disconnected cells. Version two rendered the whole table in one shadow root from data, named it with a label property rendered as a caption, used ElementInternals for the few custom parts, slotted only toolbars and empty states, and emitted events so host apps could announce through their own announcers. The same component now passes screen reader tests in all four stacks.
Who is affected, and how
Permalink to "Who is affected, and how"| User group | Boundary failure | Fix |
|---|---|---|
| Screen reader users | Unnamed grids; fragmented tables | Internal names; one-root tables |
| Keyboard users | Focus not entering components | delegatesFocus; roving focus inside |
| Low-vision users | Focus styles not themeable | Parts and custom properties |
| All users of server-rendered pages | Tables missing before JavaScript | Light-DOM enhancement or declarative shadow DOM |
Choosing between web components and framework components
Permalink to "Choosing between web components and framework components"Web components make sense when the same component must work in several frameworks or in server-rendered pages without a framework. Their accessibility cost is the boundary work described in this topic. Framework components (React, Vue, Angular, Svelte) render light DOM and avoid boundary issues entirely, but must be built once per framework. Many design systems combine both: web components for leaf controls and simple tables, framework wrappers or native framework components for complex composites such as grids and comboboxes, where relationships are dense.
Evaluating a third-party web component
Permalink to "Evaluating a third-party web component"Before adopting a web component table or grid, check: does it render a complete table or grid in one root? Can you name it with a property? Are toolbars and empty states slots rather than structural parts? Does it expose events for sort, filter and selection? Can you theme focus? Then inspect the accessibility tree in Chrome, Firefox and Safari, and spend fifteen minutes with a screen reader. Components that pass these checks rarely cause trouble later.
An audit script for existing components
Permalink to "An audit script for existing components"Search component source for aria-labelledby, aria-describedby, aria-controls and aria-activedescendant; for each, confirm the target ID lives in the same root. Search for slot elements inside table, thead or tbody. Search for setAttribute('role' in connected callbacks (candidates for ElementInternals). Most boundary bugs surface from these three searches.
Design system integration
Permalink to "Design system integration"Publish an architecture decision for table components (data-driven shadow table or light-DOM enhancement), a list of supported slots with their semantics, label and description properties, ::part names for focus and selection, and an events contract for sort, filter, selection and announcements. Include cross-browser accessibility-tree tests in the component pipeline.
Common mistakes with web component tables
Permalink to "Common mistakes with web component tables"- IDREFs across boundaries, especially for names and comboboxes.
- Nested shadow roots for rows and cells.
- Slotted structural elements.
- Unthemeable focus hidden inside shadow styles.
- Duplicate announcements between component and host.
Measuring quality
Permalink to "Measuring quality"For each web component, record: name works in all browsers (yes/no), table structure intact in the accessibility tree (yes/no), focus styles themeable (yes/no), announcements configurable (yes/no), tested in Chrome, Firefox and Safari (dates). Components failing any check should not be published as stable.
Testing checklist
Permalink to "Testing checklist"Automated
Permalink to "Automated"Keyboard
Permalink to "Keyboard"Screen reader
Permalink to "Screen reader"FAQ
Permalink to "FAQ"Are web components bad for accessibility?
No. They render real HTML. The shadow boundary requires design care for relationships and table structure.
Should table components use shadow DOM?
Either a data-driven shadow component or a light-DOM enhancement works; choose based on who owns the markup.
When will cross-root references be solved?
Proposals like reference target aim to address it; design for today’s browsers until support is broad.
Can ElementInternals fix naming?
It sets semantics on the host; names from outside still need properties or slots.
Is Svelte better than web components for accessibility?
Svelte renders light DOM by default, avoiding boundary issues, and warns at compile time. Web components are better for multi-framework reuse.
How do I theme focus inside shadow DOM?
Expose ::part() and CSS custom properties, and document required contrast.
Should components announce internally?
By default, yes, with an opt-out so host apps with central announcers can take over.
What about declarative shadow DOM?
It enables server-rendered shadow content; ensure it includes the full table and caption.
Should grids (with cell navigation) be web components?
They can be, but keep the entire grid — rows, cells, focus management — inside one shadow root, and keep composite popups (cell editors, menus) inside it or in the top layer with relationships that do not cross roots.
How do framework wrappers affect accessibility?
Wrappers should pass properties, map documented aria props to label properties, and forward events. Thin wrappers are fine; wrappers that re-render component internals cause trouble.
How do we test web components in CI?
Run Playwright against a demo page in Chromium, Firefox and WebKit projects, asserting names, roles and keyboard behaviour; add ARIA snapshots for structure.
Do open or closed shadow roots matter for accessibility?
Assistive technology sees both; testing tools and developers find open roots much easier to inspect. Prefer open roots for components you need to audit.
Is there a checklist for publishing a web component?
Name works, structure intact, focus themeable, announcements configurable, events documented, and accessibility-tree tests passing in three engines.
Related
Permalink to "Related"- React data grid libraries — framework-specific options
- Design system tokens & component contracts — contracts for shared components
- Semantic HTML table construction — table structure
- Accessible names & descriptions — naming rules
- Headless UI primitives for data — building blocks