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"
Boundaries in a web component data table Layers: the page's light DOM with headings and toolbars; the host element with semantics from ElementInternals; the shadow root holding the complete table, its caption and status region; and slots that bring in non-structural page content. Boundaries in a web component data tablePage light DOMheadings, filters, page-level announcerHost elementdefault semantics via ElementInternals; properties and eventsShadow rootcomplete table, caption, status region, focus handlingSlotscaption text, toolbar, empty state, help
Structure and references live inside the shadow root; the page contributes content through slots and properties.

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.

Architectures for framework-agnostic tables Matrix of architectures — light-DOM enhancement, data-driven shadow table, nested custom elements per row and cell, framework-compiled component — against semantics, cross-framework use, theming and risk. Architectures for framework-agnostic tablesSemanticsCross-frameworkThemingRiskLight-DOM enhancementAuthor's tableAnyPage CSSLowData-driven shadow tableOne rootAnyParts andpropertiesLowNested shadow rows and cellsBroken structureAnyComplexHighSvelte or compiled componentPlain DOMFramework-boundunless compiled tocustom elementsPage CSSLow
Avoid nested shadow roots for table structure.

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.

A host app using a web component table Sequence among host app, component host, shadow table and announcer: the host sets label and rows; the component renders the table with caption; the user sorts; the component emits sort-change; the host's announcer speaks, with internal announcements disabled. A host app using a web component tableHost appComponentShadow tableAnnouncerlabel="Invoices"; rows=[…];announce=falserender table and captionheader button activatedsort-change event"Sorted by Amount, ascending"
Clear contracts avoid duplicate messages.

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.

Permalink to "Related"

← Back to Accessible Data Component Libraries