Components Advanced
DataGrid
BetaA spreadsheet-style grid with cell navigation, in-place editing, resizable columns and virtualised rows.
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.
Overview
Name
Job title
Phone
Office
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
Amara Sato
Legal Counsel
+386 1 4001 11
London
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4005 15
London
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
Amara Sato
Legal Counsel
+386 1 4009 19
London
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
Tomás Ruiz
Account Executive
+386 1 4012 22
Ljubljana
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)))
}
/>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Name
Job title
Phone
Office
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
Amara Sato
Legal Counsel
+386 1 4001 11
London
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4005 15
London
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
Amara Sato
Legal Counsel
+386 1 4009 19
London
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
Tomás Ruiz
Account Executive
+386 1 4012 22
Ljubljana
Name
Job title
Phone
Office
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
Amara Sato
Legal Counsel
+386 1 4001 11
London
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4005 15
London
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
Amara Sato
Legal Counsel
+386 1 4009 19
London
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
Tomás Ruiz
Account Executive
+386 1 4012 22
Ljubljana
Name
Job title
Phone
Office
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
Amara Sato
Legal Counsel
+386 1 4001 11
London
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4005 15
London
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
Amara Sato
Legal Counsel
+386 1 4009 19
London
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
Tomás Ruiz
Account Executive
+386 1 4012 22
Ljubljana
Import
import { DataGrid } from '@bindshq/stance-react';Examples
10,000 rows
Rows are virtualised. Only the visible window plus an overscan is mounted.
Name
Job title
Phone
Office
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
Amara Sato
Legal Counsel
+386 1 4001 11
London
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4005 15
London
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
Amara Sato
Legal Counsel
+386 1 4009 19
London
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
Tomás Ruiz
Account Executive
+386 1 4012 22
Ljubljana
Owen Kovač
Legal Counsel
+386 1 4013 23
London
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.
Name
Job title
Phone
Office
Email
Template
Priya Nair
Account Executive
+386 1 4000 10
Ljubljana
priya.nair@binds.si
Standard signature
Amara Sato
Legal Counsel
+386 1 4001 11
London
amara.sato@binds.si
Standard signature
Lukas Haddad
Finance Lead
+386 1 4002 12
Berlin
lukas.haddad@binds.si
Standard signature
Hana Okafor
Customer Success
+386 1 4003 13
Lisbon
hana.okafor@binds.si
Standard signature
Tomás Ruiz
Account Executive
+386 1 4004 14
Ljubljana
tomás.ruiz@binds.si
Standard signature
Owen Kovač
Legal Counsel
+386 1 4005 15
London
owen.kovač@binds.si
Standard signature
Zainab Brenner
Finance Lead
+386 1 4006 16
Berlin
zainab.brenner@binds.si
Standard signature
Mateja Price
Customer Success
+386 1 4007 17
Lisbon
mateja.price@binds.si
Standard signature
Priya Nair
Account Executive
+386 1 4008 18
Ljubljana
priya.nair@binds.si
Standard signature
Amara Sato
Legal Counsel
+386 1 4009 19
London
amara.sato@binds.si
Standard signature
Lukas Haddad
Finance Lead
+386 1 4010 20
Berlin
lukas.haddad@binds.si
Standard signature
Hana Okafor
Customer Success
+386 1 4011 21
Lisbon
hana.okafor@binds.si
Standard signature
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
| Prop | Type | Default | Description |
|---|---|---|---|
columns* required | DataGridColumn<T>[] | None | id, header, accessor or cell, width, minWidth, maxWidth, editable, align. |
data* required | readonly T[] | None | Rows. Update them in onCellEdit to reflect edits. |
aria-label* required | string | None | Accessible name of the grid. |
getRowId | (row: T, index: number) => string | index | Stable row id passed to onCellEdit. |
onCellEdit | ({ rowId, columnId, value, row }) => void | None | Called when an edit is committed with a changed value. |
height | number | 400 | Viewport height in px. |
rowHeight | number | 40 | Row height in px. |
overscan | number | 6 | Rows rendered beyond the viewport. |
pinFirstColumn | boolean | false | Keep the first column visible on horizontal scroll. |
resizableColumns | boolean | true | Show resize handles in headers. |
columnWidths | Record<string, number> | None | Controlled column widths in px. |
defaultColumnWidths | Record<string, number> | None | Initial widths when uncontrolled. |
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). |
Usage 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.