RizzyUI

DataTable accessibility mode decision

RzDataTable<TItem> should remain an enhanced semantic table-composition component for future work. It should not evolve into a full ARIA grid unless a separate, explicit grid roadmap accepts the required cell-level keyboard, focus, and screen-reader obligations.

This note is specific to the current DataTable implementation: an SSR shell that serializes TanStack Table configuration into an inert JSON script, initializes a lazy Alpine runtime named rzDataTable, and lets consumers author the rendered table structure with RzTable and table subcomponents.

Decision

  • The rendered semantics are authored by consumers through ChildContent, typically with RzTable, TableHeader, TableBody, TableRow, TableHeaderCell, and TableCell.
  • The DataTable shell itself does not generate the table, rows, cells, or ARIA grid roles. It supplies state projections such as headerGroups, rows, footerGroups, and helper APIs for consumer-authored markup.
  • Native table markup already provides a strong baseline for browsing tabular data with assistive technologies when consumers use the table subcomponents correctly.
  • Interactive affordances are currently focused controls inside the table, such as buttons, checkboxes, filters, pagination links, and column visibility checkboxes, rather than a single composite grid widget.

Why not ARIA grid

A full ARIA grid changes the accessibility contract from document-style table reading to application-style composite widget navigation. If RzDataTable<TItem> declares grid semantics, the component must own a complete keyboard and focus model rather than relying on native table semantics and focusable controls.

  • Grid keyboard behavior requires deterministic arrow-key movement among cells, Home/End and Page Up/Page Down behavior, predictable entry and exit from interactive descendants, and clear behavior for sorting, selection, filtering, and pagination.
  • Grid focus management requires roving tabindex or aria-activedescendant, stable row and cell ids, focus restoration after pagination/filtering/sorting changes, and rules for virtual or missing rows.
  • Grid ARIA requires generated or enforced roles and relationships for the table, row groups, rows, column headers, row headers, grid cells, selected state, sort state, row counts, column counts, and active descendant state.
  • The current runtime uses TanStack state and consumer-authored loops. It does not own enough generated DOM to guarantee a complete grid contract across arbitrary child content.
  • Adding grid behavior now would risk duplicating or overriding the existing compliant behavior of native controls and would expand the public API beyond the current DataTable scope.

Current architecture inventory

  • SSR shell: RzDataTable.razor renders a root HtmlElement with data-slot="datatable", an Alpine host with x-data="rzDataTable", lazy loading through x-load, asset and nonce attributes, and a referenced JSON config script.
  • Inert config: RzDataTable.razor.cs validates required items, config, columns, and row id selector, normalizes columns, enforces unique row ids server-side, and serializes transport JSON into the script rather than large data attributes.
  • Config JSON shape: RzDataTableTransport.cs transports data, column definitions, initial TanStack state, feature options, row model pipeline names, and rowStructure.rowIdPath.
  • Column config: RzDataTableModels.cs supports column ids, accessor paths, header/footer/cell strings, nested columns, sorting flags, filtering flags, global filter flags, and hiding flags.
  • Row ids: both the server component and JavaScript runtime require RowIdSelector / rowStructure.rowIdPath to resolve stable, non-empty, unique row ids for TanStack getRowId.
  • Runtime: rzDataTable.js reads the script config, normalizes accessors, builds TanStack Table, exposes rendering projections, refreshes derived state, and dispatches namespaced custom events.
  • Flex rendering: rendered header, cell, and footer content is supplied to consumer-authored markup through the x-flexrender directive and the runtime _flex helper.
  • Bundle ownership: table-runtime.js exports rzDataTable, and componentBundleManifest.js maps rzDataTable to table-runtime.

Existing accessibility behavior to preserve

  • Native table structure: RzTable renders a real table inside a scroll container, while table subcomponents default to native thead, tbody, tr, th, td, caption, and tfoot elements.
  • Sorting semantics: examples bind aria-sort on header cells through sort.ariaSort(header). DataTableSortToggle binds disabled state, a dynamic next-action aria-label, a data-sort-direction hook, and click handling to sort.toggle(header).
  • Selection controls: row and select-all controls are native checkbox inputs with localized aria-label defaults, checked/disabled bindings, indeterminate state effects, and change handlers that update TanStack row selection state.
  • Selection cells: DataTableSelectionCell and DataTableSelectionHeaderCell preserve semantic td and th defaults while composing the checkbox controls.
  • Pagination examples: current examples bind disabled pagination links with aria-disabled, remove disabled controls from tab order with tabindex="-1", and mark the current page with aria-current="page".
  • Events: the runtime dispatches rz:datatable:ready, rz:datatable:state-changed, rz:datatable:selection-changed, rz:datatable:page-changed, rz:datatable:sort-changed, rz:datatable:filter-changed, and rz:datatable:column-visibility-changed with serializable state payloads and stable component ids.

Future target files

Future DataTable accessibility work should target only the existing DataTable architecture unless a later prompt explicitly expands scope.

  • src/RizzyUI/Components/DataTable/RzDataTable/RzDataTable.razor and RzDataTable.razor.cs for SSR shell attributes, config serialization, row id validation, and public parameters.
  • src/RizzyUI/Components/DataTable/RzDataTable/RzDataTableModels.cs and RzDataTableTransport.cs for config, column, initial-state, row-structure, and transport changes.
  • src/RizzyUI/Components/DataTable/RzDataTable/DataTableSortToggle.razor, DataTableSortIcon.razor, selection checkbox components, and selection cell components for focused control semantics.
  • src/RizzyUI/Components/DataTable/RzTable/ only when native table subcomponent semantics, attributes, or documentation examples require adjustment.
  • packages/rizzyui/src/js/lib/components/rzDataTable.js for TanStack state, helper APIs, event payloads, row id validation, and any future announcements or focus helpers.
  • packages/rizzyui/src/js/bundles/table-runtime.js and packages/rizzyui/src/js/runtime/componentBundleManifest.js only if runtime ownership changes are explicitly required.
  • src/RizzyUI.Docs/Components/Pages/Components/DataTableInfo.razor and this design note for consumer guidance and accessibility documentation.
  • src/RizzyUI.Tests/Components/DataTable/RzDataTable/RzDataTableTests.cs plus any future browser-level tests for behavior that bUnit cannot observe.

Recommended future changes

  1. Document the semantic-table contract directly in the DataTable API docs: consumers must author valid table structure, label external filters, bind aria-sort when rendering sortable headers, and keep row ids stable.
  2. Add optional helper guidance for screen-reader status text, such as consumer-authored polite live regions for selected row count, current page range, and filtered result count.
  3. Characterize current SSR output before refactoring: root contract, inert config script, native table composition, sort toggle attributes, selection checkbox labels and indeterminate bindings, pagination attributes in examples, and custom event payloads.
  4. Improve helper components only where behavior is incomplete or inconsistent, such as providing documented label defaults or convenience APIs for common semantic-table patterns.
  5. Avoid adding grid-only features to RzDataTable<TItem>. If an application-style grid is needed later, design it as a separate effort with a complete APG grid contract and dedicated tests.

Future test categories

  • SSR contract tests: root data-slot, Alpine host attributes, config script id, nonce, no large payloads in data attributes, and x-load behavior.
  • Transport tests: column normalization, nested column ids, duplicate ids, row id path extraction, duplicate row ids, initial sorting, pagination, filters, column visibility, and selection state serialization.
  • Semantic markup tests: examples and helper components preserve native table elements and do not add grid roles unintentionally.
  • Control accessibility tests: sort toggle disabled state, next-action labels, aria-sort guidance, checkbox labels, checked/disabled/indeterminate bindings, and pagination aria-current/aria-disabled behavior.
  • Runtime event tests: rz:datatable:* events emit stable component ids and serializable state slices for sorting, selection, pagination, filters, and column visibility.
  • Browser interaction tests: TanStack sorting, selection, filtering, pagination, and focus behavior for native controls under the Alpine runtime.
  • CSP/runtime tests: lazy bundle resolution through table-runtime, manifest ownership, and CSP-safe alternatives for examples that currently use inline Alpine expressions.

Follow-up risks

  • Because consumers author the table body, RizzyUI cannot guarantee accessible table structure unless docs, examples, and tests continue to model valid composition.
  • The docs examples currently prioritize readability with inline Alpine expressions, so CSP-safe examples require a separate follow-up.
  • There is no built-in live-region announcement strategy for changes to sorting, filtering, pagination, or selection; future work should add guidance or helpers without turning DataTable into a grid.
  • Runtime focus restoration after sorting, filtering, and pagination is not centrally managed; future improvements should preserve native control focus and avoid grid-style roving focus unless a separate grid component is designed.