Components

DataGrid

Headless ARIA grid primitives for two-dimensional cell navigation and optional row selection.

When to Use

Use DataGrid when users must move through rows and columns with the keyboard or select rows in an interactive table. Use a native table when people only need to read data; native tables are simpler and already accessible. Use TreeGrid when rows expand into a hierarchy. DataGrid does not provide sorting logic, editing, resizing, filtering, or virtualization—it exposes the states needed to connect those application features.

Features

  • Implements grid, rowgroup, row, columnheader, and gridcell semantics.
  • Keeps focus on Root and identifies the active cell with aria-activedescendant.
  • Supports controlled active cell and none, single, or multiple row selection.
  • Navigates in document order while skipping disabled cells.
  • Supports row/column counts, looping, row wrapping, and row-click selection.
  • Mirrors horizontal arrows in RTL.
  • Supports native table rendering plus asChild and render composition.

Import

import { DataGrid } from "@flowstack-ui/atom";

Anatomy

<DataGrid.Root>
  <DataGrid.Caption />
  <DataGrid.Header>
    <DataGrid.Row>
      <DataGrid.ColumnHeader />
    </DataGrid.Row>
  </DataGrid.Header>
  <DataGrid.Body>
    <DataGrid.Row>
      <DataGrid.Cell />
    </DataGrid.Row>
  </DataGrid.Body>
  <DataGrid.Footer />
</DataGrid.Root>

API Reference

All rendered parts accept the native props for their default table element and support asChild and render.

Root

Renders a focusable table, owns active-cell navigation and row selection, and provides the grid context.

PropTypeDefault
valuestring | string[] | null-
defaultValuestring | string[] | null[] for multiple; otherwise null
onValueChange(value: DataGridSelectionValue) => void-
activeCell{ rowIndex: number; columnIndex: number } | null-
defaultActiveCell{ rowIndex: number; columnIndex: number } | nullnull
onActiveCellChange(cell) => void-
selectionMode"none" | "single" | "multiple""none"
dir"ltr" | "rtl"Direction context
disabledbooleanfalse
readOnlybooleanfalse
loopbooleanfalse
wrapRowsbooleanfalse
rowCountnumber-
columnCountnumber-
selectOnRowClickbooleanfalse
asChildbooleanfalse
renderRenderProp-

Counts are truncated to positive integers. Missing or invalid counts are announced as -1, meaning the total is unknown.

ARIA attributeValues
role"grid"
aria-activedescendantGenerated active Cell ID
aria-colcountNormalized count or -1
aria-rowcountNormalized count or -1
aria-disabled"true" when disabled
aria-readonly"true" when read only
aria-multiselectable"true" in multiple mode
Data attributeValues
[data-slot]"data-grid"
[data-active]Present when an active registered Cell exists
[data-focused]Present while focus is within Root
[data-disabled]Present when disabled
[data-readonly]Present when read only
[data-column-count]Normalized supplied count
[data-row-count]Normalized supplied count
[data-selection-mode]"single" | "multiple"

Caption

Renders the native caption that names or summarizes the grid for table users.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"data-grid-caption"

Renders a thead row group for heading rows.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"rowgroup"
Data attributeValues
[data-slot]"data-grid-header"

Row

Renders a tr, supplies row metadata to its cells, and optionally participates in selection. rowIndex is one-based; zero-based index is converted to one-based.

PropTypeDefault
valuestring-
rowIndexnumber-
indexnumber-
selectablebooleantrue
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-

Rows need value to be selectable. selectable={false} keeps an identified header, footer, or summary row out of selection without disabling its cells.

ARIA attributeValues
role"row"
aria-rowindexNormalized one-based row index
aria-selectedRow selection state when selection is enabled
aria-disabled"true" when Row or Root is disabled
Data attributeValues
[data-slot]"data-grid-row"
[data-selectable]Present for a selectable valued row
[data-selection-disabled]Present when the row opts out of enabled selection
[data-row-index]Normalized index
[data-value]Row value
[data-selected]Present when selected
[data-disabled]Present when disabled

ColumnHeader

Renders a th registered as a navigable cell. It exposes sort state but does not change sorting when clicked.

PropTypeDefault
columnIndexnumber-
indexnumber-
disabledbooleanfalse
sortDirection"ascending" | "descending" | "none" | "other"-
scopenative th scope"col"
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"columnheader"
aria-colindexNormalized one-based column index
aria-sortValue from sortDirection
aria-selectedParent Row selection state when enabled
aria-disabled"true" when explicitly, row, or grid disabled
Data attributeValues
[data-slot]"data-grid-column-header"
[data-column-index]Normalized index
[data-sort]Sort direction when supplied
[data-active]Present when active and Root is focused
[data-selected]Present when its Row is selected
[data-disabled]Present when explicitly, row, or grid disabled

Body

Renders a tbody row group for the main data rows.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"rowgroup"
Data attributeValues
[data-slot]"data-grid-body"

Cell

Renders a registered td. Clicking an enabled indexed Cell makes it active and focuses Root; missing indexes make it non-navigable without announcing it as disabled.

PropTypeDefault
columnIndexnumber-
indexnumber-
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"gridcell"
aria-colindexNormalized one-based column index
aria-selectedParent Row selection state when enabled
aria-disabled"true" when explicitly, row, or grid disabled
Data attributeValues
[data-slot]"data-grid-cell"
[data-column-index]Normalized index
[data-active]Present when active and Root is focused
[data-selected]Present when its Row is selected
[data-disabled]Present when explicitly, row, or grid disabled

Renders a tfoot row group for totals and summary rows.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"rowgroup"
Data attributeValues
[data-slot]"data-grid-footer"

Advanced custom parts can use the public useDataGridContext and useDataGridRowContext hooks. The row hook returns null outside Row; the main hook must be used inside Root. Prefer the namespaced parts for standard grids.

Examples

Selectable Project Grid

import { DataGrid } from "@flowstack-ui/atom";

const projects = [
  { id: "alpha", name: "Alpha", status: "Ready" },
  { id: "bravo", name: "Bravo", status: "Review" },
];

export function ProjectGrid() {
  return (
    <DataGrid.Root
      aria-label="Projects"
      rowCount={projects.length + 1}
      columnCount={2}
      selectionMode="multiple"
      defaultValue={["alpha"]}
      selectOnRowClick
    >
      <DataGrid.Caption>Current projects</DataGrid.Caption>
      <DataGrid.Header>
        <DataGrid.Row rowIndex={1} selectable={false}>
          <DataGrid.ColumnHeader columnIndex={1}>Name</DataGrid.ColumnHeader>
          <DataGrid.ColumnHeader columnIndex={2}>Status</DataGrid.ColumnHeader>
        </DataGrid.Row>
      </DataGrid.Header>
      <DataGrid.Body>
        {projects.map((project, index) => (
          <DataGrid.Row key={project.id} rowIndex={index + 2} value={project.id}>
            <DataGrid.Cell columnIndex={1}>{project.name}</DataGrid.Cell>
            <DataGrid.Cell columnIndex={2}>{project.status}</DataGrid.Cell>
          </DataGrid.Row>
        ))}
      </DataGrid.Body>
    </DataGrid.Root>
  );
}

Controlled Sort Metadata

import { useState } from "react";
import { DataGrid, type DataGridSortDirection } from "@flowstack-ui/atom";

export function SortableHeader() {
  const [direction, setDirection] = useState<DataGridSortDirection>("ascending");

  return (
    <DataGrid.Root aria-label="People" rowCount={1} columnCount={1}>
      <DataGrid.Header>
        <DataGrid.Row rowIndex={1} selectable={false}>
          <DataGrid.ColumnHeader columnIndex={1} sortDirection={direction}>
            <button
              type="button"
              onClick={() => setDirection(
                direction === "ascending" ? "descending" : "ascending",
              )}
            >
              Name
            </button>
          </DataGrid.ColumnHeader>
        </DataGrid.Row>
      </DataGrid.Header>
    </DataGrid.Root>
  );
}

Accessibility

DataGrid follows the WAI-ARIA Grid pattern. Provide an accessible name with Caption, aria-label, or aria-labelledby. Every navigable row and cell needs a valid one-based index. Root receives DOM focus while aria-activedescendant identifies the active Cell.

KeyDescription
ArrowRight / ArrowLeftMoves within a row; directions mirror in RTL.
ArrowDown / ArrowUpMoves by row, preferring the same column and skipping disabled cells.
Home / EndMoves to the first or last enabled cell in the current row.
Ctrl+Home / Cmd+HomeMoves to the first enabled grid cell.
Ctrl+End / Cmd+EndMoves to the last enabled grid cell.
Enter / SpaceToggles/selects the active Cell's Row when selection is enabled.

readOnly prevents selection changes but still permits navigation. disabled prevents Root keyboard handling. Rows with selectable={false} ignore row selection from click, Enter, and Space.

Changelog

See CHANGELOG.md.