Components / structure

DataGrid

The APG draws the same line: its own layout-grids example is excluded from the port as a use of the pattern rather than the pattern.

Rendered from the package · open on its own

Summary

A table whose cells take focus — arrow keys move in two dimensions and the whole grid costs one tab stop.

APG patternGrid
Built onnative elements
Statusstable
Since0.2.0
<DataGrid label="Section layout" columns={…} rows={sections} onCellActivate={(row, col) => edit(row, col)} />

The export is DataGrid and the anatomy stays data-se="grid". Grid names the layout primitive, which places children and carries no role, no keyboard model and no announcement.

Anatomy

A table carrying role="grid", shared with Treegrid so one set of rules dresses both.

PartElementRequired
cell<td>yes
columnheader<th>yes
row<tr>yes

Properties

PropertyValuesDefaultWhat it does
columnsDataGridColumn[]requiredThe columns, in display order. Mark the one that identifies its row as header so a screen reader can say which row a value belongs to.
rowsRecord<string, unknown>[]requiredOne object per row, keyed by column key.
labelstringrequiredNames the grid. A grid is a place a reader enters, and entering an unnamed one tells them nothing.
getRowId(row: Record<string, unknown>) => stringnoneA stable id per row. Without it the grid keys on index, which moves the focused cell when rows are inserted above it.
onCellActivate(row: Record<string, unknown>, column: DataGridColumn) => voidnoneCalled when a cell is activated with Enter. This is the hook for interaction mode — what happens inside the cell is the caller's.
rowCountnumbernoneTotal rows in the dataset when this grid renders only a window over it. Sets aria-rowcount/aria-colcount and switches rows to true aria-rowindex — without it a virtualised grid announces "1 of 20" instead of "101 of 340".
rowOffsetnumber0Index of the first rendered row within that dataset.

States

StateExpressed as
Focused:focus-visible
Selectedaria-selected

Behavior

Two modes. In navigation mode the arrows move between cells and the grid is one tab stop. In interaction mode the arrows belong to whatever widget is inside the cell.

Home and End go to the row's ends, Ctrl with either to the grid's.

Accessibility

A column marked header renders role="rowheader", so a screen reader can say which row a value belongs to instead of reading a bare number.

Only when cells are genuinely navigable — a spreadsheet, a date picker, a matrix of settings. A table that is merely read is a DataTable; putting grid semantics on it turns every cell into an obstacle for no gain, and a screen-reader user then has to escape a grid they never wanted to enter.

Keyboard

KeyResult
Arrow keysMove one cell in two dimensions
Home / EndJump to the ends of the row
Ctrl + Home / EndJump to the ends of the grid
EnterActivates the cell, entering interaction mode
EscapeReturns to navigation mode

Roles, states and properties

AttributeSet by
aria-colcountset at render
aria-colindexset at render
aria-labelset at render
aria-rowcountset at render
aria-rowindexset at render
rolealways grid, always row, always columnheader, set at render

Focus

One tab stop for the whole grid. Two modes: in navigation mode the arrows move between cells; in interaction mode they belong to whatever widget the cell holds.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It sets no target size of its own; anything interactive inside it brings its own.

Tested

  • test/apg/batch-c.test.jsx
  • test/apg/partials-sweep.test.jsx
  • test/apg/table-narrowing.test.jsx
  • test/browser/axe.spec.mjs
  • test/browser/keyboard.spec.mjs
  • test/browser/site.spec.mjs
  • test/browser/upstream-combobox.spec.mjs
  • test/browser/upstream-composites.spec.mjs
  • test/browser/upstream-forms-menubar.spec.mjs
  • test/browser/upstream-overlays.spec.mjs
  • test/browser/upstream-toolbar-tree.spec.mjs

Known gaps

  • 3 upstream refs — layout grids — a use of the pattern, not the pattern

Appearance

TokenUsed for
--se-accent-softbackground
--se-borderborder, border-block-end
--se-border-widthborder, border-block-end
--se-focus-coloroutline
--se-focus-widthoutline, outline-offset
--se-radius-mdborder-radius
--se-space-2padding
--se-space-3padding
--se-surface-selectedbackground
--se-surface-sunkbackground
--se-textcolor
--se-text-mutedcolor
--se-text-smfont-size

Motion

None.

Content

Column labels are nouns. A grid is read cell by cell, so a header that needs a sentence will be heard as one on every cell beneath it.

Usage

If your rows form a hierarchy, that is a Treegrid. If the cells are only read, that is a DataTable.

Notes

Renamed from Grid on 2026-08-22. The two were unrelated things sharing a word, and a consumer reaching for a tile layout and finding this one would ship a page whose reader has to escape a grid. The anatomy did not move — data-se="grid" is what a Vue or Astro consumer writes and what Treegrid renders — so no CSS rule changed and only the export broke, at 0.2.0 where a clean break costs a minor bump.

The APG draws the same line: its own layout-grids example is excluded from the port as a use of the pattern rather than the pattern.