Designing Accessibility Props for Grid Components

Permalink to "Designing Accessibility Props for Grid Components"

A component’s props are its accessibility contract with product teams. If label is optional, half the grids in production will be unnamed. If announcement wording is hard-coded in English, every translated product will speak English. If the only way to name a row is a free-form aria-label prop on each row, nobody will do it. Designing the API so the accessible path is the default — and the inaccessible path takes effort — is the most leveraged accessibility work a design system team can do.

This guide belongs to design system tokens & component contracts.

Spec reference

Permalink to "Spec reference"

The props map to requirements elsewhere on this site:

  • Name — SC 4.1.2; grids need an accessible name.
  • Row headers — SC 1.3.1; rows are announced by their header cell.
  • Announcements — SC 4.1.3; sort, filter, selection, pagination, loading.
  • Density and targets — SC 2.5.8; default density keeps 24 px targets.
  • Focus — SC 2.4.7, 2.4.11; visible, unobscured focus.
Layers of a grid component's API Layers: required props (label, rowHeader); typed message props with defaults; behaviour defaults (density, virtualization, focus); and escape hatches (aria overrides, custom renderers with focus contracts). Layers of a grid component's APIRequired propslabel or labelledBy; rowHeader columnMessagessorted, filtered, selected, page, loading, error templatesDefaultsstandard density; visible focus; announcer onEscape hatchesariaDescribedBy; renderer focus contract; announcer override
Required props prevent the common failures; escape hatches stay narrow and documented.

When to use — and when not to

Permalink to "When to use — and when not to"

Design accessibility props for any component that products configure heavily: grids, tables, charts, comboboxes, date pickers. Make the critical ones required at the type level (TypeScript) and at runtime (development warnings).

Do not expose a generic ariaProps spread for every internal element; it invites misuse and makes behaviour unpredictable. Do not require props that most products cannot meaningfully fill — every required prop must have an obvious, correct value.

The misapplication: an optional ariaLabel prop buried among 80 others, with no warning when missing.

Optional free-form props versus designed props Comparison: optional ariaLabel and per-row aria props lead to unnamed grids and unlabelled rows; required label and rowHeader props, typed messages and safe defaults produce accessible output by default. Optional free-form props versus designed props✗ Optional, free-formariaLabel?: string (often missing)Per-row aria-label in render codeHard-coded English announcementsCompact density by defaultaria props spread anywhere✓ Designed contractlabel or labelledBy requiredrowHeader: 'id' derives namesmessages prop with i18n defaultsStandard density defaultNarrow, documented escape hatches

Annotated code example

Permalink to "Annotated code example"
// Grid props: the accessible path is the typed, required path
type Label = { label: string; labelledBy?: never } | { labelledBy: string; label?: never };

interface GridMessages {
  sorted?(column: string, direction: 'ascending' | 'descending'): string;
  filtered?(visible: number, total: number): string;
  selected?(count: number): string;
  page?(page: number, pages: number): string;
  loading?(): string; error?(): string;
}

type GridProps<Row> = Label & {
  rows: Row[];
  columns: Column<Row>[];
  rowHeader: keyof Row;                 // required: rows get names (SC 1.3.1)
  messages?: GridMessages;              // defaults from i18n (SC 4.1.3)
  density?: 'standard' | 'comfortable' | 'compact';   // default 'standard' keeps 24 px targets
  announcer?: 'internal' | ((msg: string) => void);   // escape hatch for central announcers
};

// Runtime guard in development
if (process.env.NODE_ENV !== 'production' && !props.label && !props.labelledBy) {
  console.error('[Grid] An accessible name is required: pass label or labelledBy.');
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Prop Effect users notice
label / labelledBy Grid announced with its name
rowHeader Row announcements name the record
messages.sorted “Sorted by Amount, ascending” in the user’s language
density default Targets large enough for pointer users
announcer One voice for the whole product
Prop design choices and their effect on products Matrix of choices — label required, rowHeader required, messages with i18n defaults, density default standard, narrow escape hatches — against unnamed grids, unnamed rows, wrong-language messages, small targets and misuse. Prop design choices and their effect on productsUnnamed gridsUnnamed rowsWronglanguageSmall targetsMisuselabel requiredPreventedNoNoNoNorowHeader requiredNoPreventedNoNoNomessages with i18nNoNoPreventedNoNodensity defaultNoNoNoPreventedNoNarrow escape hatchesNoNoNoNoReduced
Each choice removes one common failure.

Integration context

Permalink to "Integration context"

Props feed the patterns described across this site: naming in labelling grids with aria-labelledby, announcements in screen reader announcement strategies, density in density tokens and target size in data grids. Documenting them is covered in documenting keyboard contracts for components.

Gotchas

Permalink to "Gotchas"

Breaking changes. Making a prop required is a breaking change; ship it with a deprecation period and warnings first (see versioning guidance).

labelledBy across shadow roots. If your component uses shadow DOM, labelledBy cannot reference page IDs; accept text instead.

Renderer contracts. Custom cell renderers need a documented focus contract (receive tabIndex, never add their own tab stops).

Worked example

Permalink to "Worked example"

A design system’s grid had an optional ariaLabel and no row header concept. An audit of 40 product grids found 31 unnamed and 38 without row headers. Version 3 introduced a required label | labelledBy union, a required rowHeader, translatable messages with defaults and a dev-time warning. A codemod filled label from nearby headings in most cases. Six months later, all 40 grids were named and announced rows by record.

Common mistakes

Permalink to "Common mistakes"
  • Optional names with no warning when missing.
  • Per-row aria props that teams must remember to fill.
  • Hard-coded English in announcements.
  • Generic ARIA pass-through that conflicts with internal semantics.
  • Escape hatches without docs, so teams use them inconsistently.

Adoption checklist

Permalink to "Adoption checklist"
  1. Audit current usage: how many instances set a name and row header today?
  2. Introduce the new props alongside the old ones, with warnings.
  3. Ship a codemod that infers labels from nearby headings and row headers from ID-like columns.
  4. Update documentation examples so every snippet shows the required props.
  5. Make the props required at the type level in the next major version.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Review every new prop for accessibility impact in API review, and keep a short “accessibility props” section at the top of each component’s documentation.

FAQ

Permalink to "FAQ"
Should aria-* props be passed through to the grid element?

Only a few, documented ones (such as ariaDescribedBy). Generic pass-through invites conflicts with internal semantics.

Is a required label annoying for teams?

It takes seconds to fill and prevents the most common failure. Pair it with a codemod for existing usage.

How do messages handle plurals?

Use functions that receive counts, implemented with your i18n library’s plural rules.

What if a product needs a different announcer?

Accept a callback (announcer prop) so products with central announcers can route messages.

Should density be a prop or a theme setting?

Both: a prop with a theme-level default and user preference support.

How do I enforce props in JavaScript projects?

Runtime warnings in development, and lint rules for JSX usage.

What about charts?

The same principles: required title and summary props, table support and series style defaults.

How do props interact with design tokens?

Props choose behaviour (density, announcer); tokens supply values (sizes, colours). Keep values out of props so themes can change them centrally.

Should the grid accept a description?

Yes, an optional description or describedBy for context such as filters applied, rendered as aria-describedby on the grid element.

Permalink to "Related"

← Back to Design System Tokens & Component Contracts