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.
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.
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 |
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"- Audit current usage: how many instances set a name and row header today?
- Introduce the new props alongside the old ones, with warnings.
- Ship a codemod that infers labels from nearby headings and row headers from ID-like columns.
- Update documentation examples so every snippet shows the required props.
- 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.
Related
Permalink to "Related"- Density tokens and target size — the density default
- Documenting keyboard contracts — documenting behaviour
- Versioning and deprecating inaccessible APIs — changing props safely