# DataGrid

> A spreadsheet-style grid with cell navigation, in-place editing, resizable columns and virtualised rows.

Category: Advanced · Status: beta · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/data-grid

Use DataGrid when people work inside the data: bulk-editing signature fields for hundreds of mailboxes, or reviewing large imports. It implements the WAI-ARIA grid pattern with a single tab stop and arrow-key navigation, and only renders visible rows. For read-mostly collections with sorting and selection, use DataTable.

## Import

```tsx
import { DataGrid } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Editable grid

```tsx
import * as React from 'react';
import { DataGrid } from '@bindshq/stance-react';
import type { DataGridColumn } from '@bindshq/stance-react';

interface Field {
  id: string;
  name: string;
  title: string;
  phone: string;
  office: string;
}

const offices = ['Ljubljana', 'London', 'Berlin', 'Lisbon'];

const titles = ['Account Executive', 'Legal Counsel', 'Finance Lead', 'Customer Success'];

const first = ['Priya', 'Amara', 'Lukas', 'Hana', 'Tomás', 'Owen', 'Zainab', 'Mateja'];

const last = ['Nair', 'Okafor', 'Brenner', 'Sato', 'Ruiz', 'Price', 'Haddad', 'Kovač'];

function makeFields(count: number): Field[] {
  return Array.from({ length: count }, (_, i) => ({
    id: `m${i}`,
    name: `${first[i % 8]} ${last[(i * 3) % 8]}`,
    title: titles[i % 4]!,
    phone: `+386 1 ${String(4000 + i).padStart(4, '0')} ${String(10 + (i % 90))}`,
    office: offices[i % 4]!,
  }));
}

const columns: DataGridColumn<Field>[] = [
  { id: 'name', header: 'Name', accessor: 'name', width: 180 },
  { id: 'title', header: 'Job title', accessor: 'title', editable: true, width: 200 },
  { id: 'phone', header: 'Phone', accessor: 'phone', editable: true, width: 170 },
  { id: 'office', header: 'Office', accessor: 'office', editable: true, width: 140 },
];

export function Basic() {
  const [data, setData] = React.useState(() => makeFields(40));
  return (
    <DataGrid
      aria-label="Signature fields"
      columns={columns}
      data={data}
      getRowId={(r) => r.id}
      height={320}
      onCellEdit={({ rowId, columnId, value }) =>
        setData((rows) => rows.map((r) => (r.id === rowId ? { ...r, [columnId]: value } : r)))
      }
    />
  );
}
```

### 10,000 rows

Rows are virtualised. Only the visible window plus an overscan is mounted.

```tsx
import * as React from 'react';
import { DataGrid } from '@bindshq/stance-react';
import type { DataGridColumn } from '@bindshq/stance-react';

interface Field {
  id: string;
  name: string;
  title: string;
  phone: string;
  office: string;
}

const offices = ['Ljubljana', 'London', 'Berlin', 'Lisbon'];

const titles = ['Account Executive', 'Legal Counsel', 'Finance Lead', 'Customer Success'];

const first = ['Priya', 'Amara', 'Lukas', 'Hana', 'Tomás', 'Owen', 'Zainab', 'Mateja'];

const last = ['Nair', 'Okafor', 'Brenner', 'Sato', 'Ruiz', 'Price', 'Haddad', 'Kovač'];

function makeFields(count: number): Field[] {
  return Array.from({ length: count }, (_, i) => ({
    id: `m${i}`,
    name: `${first[i % 8]} ${last[(i * 3) % 8]}`,
    title: titles[i % 4]!,
    phone: `+386 1 ${String(4000 + i).padStart(4, '0')} ${String(10 + (i % 90))}`,
    office: offices[i % 4]!,
  }));
}

const columns: DataGridColumn<Field>[] = [
  { id: 'name', header: 'Name', accessor: 'name', width: 180 },
  { id: 'title', header: 'Job title', accessor: 'title', editable: true, width: 200 },
  { id: 'phone', header: 'Phone', accessor: 'phone', editable: true, width: 170 },
  { id: 'office', header: 'Office', accessor: 'office', editable: true, width: 140 },
];

export function LargeDataset() {
  const data = React.useMemo(() => makeFields(10000), []);
  return (
    <DataGrid
      aria-label="All mailboxes"
      columns={columns.map((c) => ({ ...c, editable: false }))}
      data={data}
      getRowId={(r) => r.id}
      height={360}
    />
  );
}
```

### Pinned first column

The first column stays visible while scrolling horizontally.

```tsx
import { DataGrid } from '@bindshq/stance-react';
import type { DataGridColumn } from '@bindshq/stance-react';

interface Field {
  id: string;
  name: string;
  title: string;
  phone: string;
  office: string;
}

const offices = ['Ljubljana', 'London', 'Berlin', 'Lisbon'];

const titles = ['Account Executive', 'Legal Counsel', 'Finance Lead', 'Customer Success'];

const first = ['Priya', 'Amara', 'Lukas', 'Hana', 'Tomás', 'Owen', 'Zainab', 'Mateja'];

const last = ['Nair', 'Okafor', 'Brenner', 'Sato', 'Ruiz', 'Price', 'Haddad', 'Kovač'];

function makeFields(count: number): Field[] {
  return Array.from({ length: count }, (_, i) => ({
    id: `m${i}`,
    name: `${first[i % 8]} ${last[(i * 3) % 8]}`,
    title: titles[i % 4]!,
    phone: `+386 1 ${String(4000 + i).padStart(4, '0')} ${String(10 + (i % 90))}`,
    office: offices[i % 4]!,
  }));
}

const columns: DataGridColumn<Field>[] = [
  { id: 'name', header: 'Name', accessor: 'name', width: 180 },
  { id: 'title', header: 'Job title', accessor: 'title', editable: true, width: 200 },
  { id: 'phone', header: 'Phone', accessor: 'phone', editable: true, width: 170 },
  { id: 'office', header: 'Office', accessor: 'office', editable: true, width: 140 },
];

export function PinnedColumn() {
  const wide: DataGridColumn<Field>[] = [
    ...columns,
    {
      id: 'email',
      header: 'Email',
      accessor: (r) => `${r.name.toLowerCase().replace(/\s+/g, '.')}@binds.si`,
      width: 260,
    },
    { id: 'template', header: 'Template', accessor: () => 'Standard signature', width: 200 },
  ];
  return (
    <div style={{ maxWidth: 560 }}>
      <DataGrid
        aria-label="Mailboxes"
        columns={wide}
        data={makeFields(30)}
        getRowId={(r) => r.id}
        height={280}
        pinFirstColumn
      />
    </div>
  );
}
```

## API reference

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| DataGrid | `columns` (required) | `DataGridColumn<T>[]` | None | id, header, accessor or cell, width, minWidth, maxWidth, editable, align. |
| DataGrid | `data` (required) | `readonly T[]` | None | Rows. Update them in onCellEdit to reflect edits. |
| DataGrid | `aria-label` (required) | `string` | None | Accessible name of the grid. |
| DataGrid | `getRowId` | `(row: T, index: number) => string` | `index` | Stable row id passed to onCellEdit. |
| DataGrid | `onCellEdit` | `({ rowId, columnId, value, row }) => void` | None | Called when an edit is committed with a changed value. |
| DataGrid | `height` | `number` | `400` | Viewport height in px. |
| DataGrid | `rowHeight` | `number` | `40` | Row height in px. |
| DataGrid | `overscan` | `number` | `6` | Rows rendered beyond the viewport. |
| DataGrid | `pinFirstColumn` | `boolean` | `false` | Keep the first column visible on horizontal scroll. |
| DataGrid | `resizableColumns` | `boolean` | `true` | Show resize handles in headers. |
| DataGrid | `columnWidths` | `Record<string, number>` | None | Controlled column widths in px. |
| DataGrid | `defaultColumnWidths` | `Record<string, number>` | None | Initial widths when uncontrolled. |
| DataGrid | `onColumnWidthsChange` | `(widths: Record<string, number>) => void` | None | Called when a column is resized. |

## Accessibility

- Implements the WAI-ARIA grid pattern: role="grid" with rows, column headers and grid cells, plus aria-rowcount and aria-rowindex so virtualised rows report their true position.
- The grid is a single tab stop; arrow keys move between cells (roving tabindex).
- Read-only cells in an editable grid are marked aria-readonly.
- Column resize handles are focusable separators with aria-valuenow, reachable with Enter from the header cell.
- Committed and cancelled edits are announced in a polite live region.

## Keyboard

| Keys | Action |
| --- | --- |
| `←` `→` `↑` `↓` | Move to the adjacent cell. |
| `Home` `End` | First or last cell in the row. |
| `Ctrl` `Home` | First cell of the grid (header row). |
| `Ctrl` `End` | Last cell of the grid. |
| `Page Down` `Page Up` | Move one visible page of rows. |
| `Enter` `F2` | Edit the cell. On a header cell, focus its resize handle. |
| `Any character` | Start editing an editable cell, replacing its value. |
| `Enter` | While editing: commit. |
| `Escape` | While editing: cancel. On a resize handle: return to the header. |
| `←` `→` | On a resize handle: resize by 10px (Shift: 50px). |

## Guidelines

**Do**

- Mark only the columns people should change as editable.
- Persist edits optimistically and surface failures inline.

**Don’t**

- Use DataGrid for simple lists; a DataTable is easier to read and navigate.
- Put complex widgets in cells; link out to a detail view instead.

## Related

- https://stanceui.com/components/data-table
- https://stanceui.com/components/virtualized-list
- https://stanceui.com/components/table
