Skip to content
weftGitHub

DataGrid

A table you work in like a spreadsheet: move by cell, select ranges, edit, group and export.

SourcePackage @weft-js/warpPlayground

Preview

tsx
import { Badge, DataGrid } from '@weft-js/warp'import type { GridColumn } from '@weft-js/warp'export function Basic() {  return (    <DataGrid caption="Projects" captionHidden rows={PROJECTS} columns={GRID} exportName="weft-projects" />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, filter: 'text' },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true, filter: 'text' },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, filter: 'min', aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, filter: 'min', aggregate: 'sum', format: v => money(Number(v)) },]const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Installation

npm install @weft-js/warp

Usage

tsx
import { DataGrid } from '@weft-js/warp'<DataGrid caption="Projects" captionHidden rows={PROJECTS} columns={GRID} exportName="weft-projects" />

Editing

Enter or a double-click edits. A bad value never commits. Paste a block from a spreadsheet and it lands cell by cell.

tsx
import { state } from '@weft-js/core'import { DataGrid, Stack, Text } from '@weft-js/warp'import type { GridColumn } from '@weft-js/warp'export function Edit() {  const rows = state(PROJECTS)  const last = state('Nothing changed yet.')  const save = (row: Project, column: string, value: unknown) => {    rows.value = rows.value.map(p => (p.id === row.id ? { ...p, [column]: value } : p))    last.value = `${row.name}: ${column} is now ${String(value)}.`  }  return (    <Stack gap="related">      <Text subtle>{last.value}</Text>      <DataGrid caption="Budgets" captionHidden rows={rows} columns={NUMBERS} onEdit={save} exportName={false} />    </Stack>  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const NUMBERS: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, sort: true, edit: true, validate: v => String(v).trim() !== '' || 'A project needs a name' },  { id: 'owner', header: 'Owner', sort: true, edit: true },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, aggregate: 'sum' },  { id: 'cost', header: 'Budget', numeric: true, sort: true, aggregate: 'sum', format: v => money(Number(v)), edit: text => Number(text.replace(/[$,]/g, '')), validate: v => (typeof v === 'number' && Number.isFinite(v) && v >= 0) || 'Enter an amount' },]const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Groups and totals

Press Group to toggle. aggregate fills each group's subtotal and the footer.

tsx
import { Badge, DataGrid } from '@weft-js/warp'import type { GridColumn, GridView } from '@weft-js/warp'export function Groups() {  return (    <DataGrid caption="Projects by region" captionHidden rows={PROJECTS} columns={GROUPED_GRID} groupBy="region" views={GROUPED} exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const spend = (rows: Project[]) => `Total ${money(rows.reduce((n, p) => n + p.cost, 0))}`const GROUPED_GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, aggregate: spend },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true },  { id: 'cost', header: 'Cost', numeric: true, sort: true, format: v => money(Number(v)) },]const GROUPED: Record<string, GridView> = { 'By region': { order: ['name', 'status', 'region', 'owner', 'cost'], hidden: [], filters: {}, group: true } }const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Saved views

A view holds columns, filters, sort and grouping. Pick one, change it, then Save view.

tsx
import { Badge, DataGrid } from '@weft-js/warp'import type { GridColumn, GridView } from '@weft-js/warp'export function Views() {  return (    <DataGrid caption="Projects" captionHidden rows={PROJECTS} columns={GRID} views={VIEWS} exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, filter: 'text' },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true, filter: 'text' },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, filter: 'min', aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, filter: 'min', aggregate: 'sum', format: v => money(Number(v)) },]const ORDER = ['name', 'status', 'region', 'owner', 'deploys', 'cost']const VIEWS: Record<string, GridView> = {  'All projects': { order: ORDER, hidden: [], filters: {} },  'Needs attention': { order: ORDER, hidden: ['deploys'], filters: { status: 'Failed' } },  'Costly': { order: ['name', 'cost', 'owner', 'region'], hidden: ['status', 'deploys'], filters: { cost: '1000' }, sort: { id: 'cost', dir: 'descending' } },}const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

A tree

Rows with kids become branches, and each parent rolls up its children. Filtering keeps the path to each match.

tsx
import { DataGrid } from '@weft-js/warp'import type { GridColumn, GridTree } from '@weft-js/warp'export function Tree() {  return (    <DataGrid caption="Spend by team" captionHidden rows={SPEND} columns={SPEND_COLUMNS} tree={OWN_KIDS} filter="Filter teams and projects" exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const SPEND: Spend[] = [  team('Platform', 'billing', 'notifier', 'edge-cache', 'auth'),  team('Web', 'storefront', 'checkout'),  team('Data', 'search-api', 'reports', 'ingest'),  team('Mobile', 'mobile-bff'),]const SPEND_COLUMNS: GridColumn<Spend>[] = [  { id: 'name', header: 'Team and project', sort: true },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, aggregate: 'sum', format: v => money(Number(v)) },]const OWN_KIDS: GridTree<Spend> = {}const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

A tree that loads

load fetches a branch on its first open.

tsx
import { DataGrid } from '@weft-js/warp'import type { GridColumn, GridTree } from '@weft-js/warp'export function LazyTree() {  return (    <DataGrid caption="Spend by team" captionHidden rows={LAZY} columns={SPEND_COLUMNS} tree={LOAD} exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const SPEND: Spend[] = [  team('Platform', 'billing', 'notifier', 'edge-cache', 'auth'),  team('Web', 'storefront', 'checkout'),  team('Data', 'search-api', 'reports', 'ingest'),  team('Mobile', 'mobile-bff'),]const SPEND_COLUMNS: GridColumn<Spend>[] = [  { id: 'name', header: 'Team and project', sort: true },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, aggregate: 'sum', format: v => money(Number(v)) },]const LAZY: Spend[] = SPEND.map(t => ({ ...t, kids: undefined, hasChildren: true, deploys: t.kids!.reduce((n, k) => n + k.deploys, 0), cost: t.kids!.reduce((n, k) => n + k.cost, 0) }))const LOAD: GridTree<Spend> = {  branch: row => !!row.hasChildren,  load: async row => {    await new Promise(done => { setTimeout(done, 700) })    return SPEND.find(t => t.id === row.id)?.kids ?? []  },}const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Bulk actions

Shift-select across rows and the bar appears over them.

tsx
import { state } from '@weft-js/core'import { Badge, Button, DataGrid } from '@weft-js/warp'import type { GridColumn } from '@weft-js/warp'export function Bulk() {  const rows = state(PROJECTS)  const pause = (keys: string[]) => (rows.value = rows.value.map(p => (keys.includes(p.id) ? { ...p, status: 'Paused' as const } : p)))  return <DataGrid caption="Projects" captionHidden rows={rows} columns={GRID} exportName={false} bulk={keys => <Button size="sm" emphasis="outline" tone="neutral" icon="pause" onClick={() => pause(keys)}>Pause</Button>} />}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, filter: 'text' },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true, filter: 'text' },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, filter: 'min', aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, filter: 'min', aggregate: 'sum', format: v => money(Number(v)) },]const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Row details

Use expand to open extra detail under a row.

tsx
import { Badge, DataGrid, Row, Stack, Text } from '@weft-js/warp'import type { GridColumn } from '@weft-js/warp'export function Details() {  return (    <DataGrid caption="Projects" captionHidden rows={PROJECTS} columns={GRID} expand={details} exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const details = (p: Project) => (  <Row gap="section">    <Stack gap="hair"><Text role="label" subtle>Uptime</Text><Text>{p.uptime}%</Text></Stack>    <Stack gap="hair"><Text role="label" subtle>Latency</Text><Text>{p.latency} ms</Text></Stack>    <Stack gap="hair"><Text role="label" subtle>Updated</Text><Text>{p.updated}</Text></Stack>  </Row>)const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, filter: 'text' },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true, filter: 'text' },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, filter: 'min', aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, filter: 'min', aggregate: 'sum', format: v => money(Number(v)) },]const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

Pages

Use page to show a few rows at a time, here four.

tsx
import { Badge, DataGrid } from '@weft-js/warp'import type { GridColumn } from '@weft-js/warp'export function Paged() {  return (    <DataGrid caption="Projects" captionHidden rows={PROJECTS} columns={GRID} page={FOUR} exportName={false} />  )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [  { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' },  { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' },  { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' },  { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' },  { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' },  { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' },  { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' },  { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' },  { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' },  { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }type Project = (typeof PROJECTS)[number]const money = (n: number) => `$${n.toLocaleString('en-US')}`const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const STATUSES = ['Live', 'Building', 'Failed', 'Paused']const REGION_NAMES = ['eu-west', 'eu-north', 'us-east', 'us-west', 'ap-south', 'global']const GRID: GridColumn<Project>[] = [  { id: 'name', header: 'Project', rowHeader: true, pin: true, sort: true, filter: 'text' },  { id: 'status', header: 'Status', sort: true, filter: 'select', options: STATUSES, cell: badge },  { id: 'region', header: 'Region', sort: true, filter: 'select', options: REGION_NAMES },  { id: 'owner', header: 'Owner', sort: true, filter: 'text' },  { id: 'deploys', header: 'Deploys', numeric: true, sort: true, filter: 'min', aggregate: 'sum' },  { id: 'cost', header: 'Cost', numeric: true, sort: true, filter: 'min', aggregate: 'sum', format: v => money(Number(v)) },]const FOUR = { size: 4 }const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260

When to use

Use it for

  • People edit or copy many cells
  • Analysts who filter, group and save views

Reach for something else

  • Table, when people mostly read rows
  • ScrollGrid, when it is read-only and very large
  • PivotTable, when you want totals across two dimensions

Playground

Density
Group by
Export name
import { DataGrid } from '@weft-js/warp'<DataGrid caption="Projects" rows={projects} columns={columns} groupBy="region" />

Accessibility

↑↓←→
Moves the cell
Shift+arrows
Extends the range
⌘C
Copies the range as tab-separated text
⌘V
Pastes into editable cells
EnterorF2
Edits the cell
Esc
Cancels the edit
Alt←→
Moves the column
→←
In a tree, opens and closes a branch
  • One tab stop, then arrows: the APG grid pattern, or treegrid when grouped or hierarchical.
  • Moves, edits and exports are announced.
  • An invalid edit stays open and says why.

API Reference

PropTypeDefaultDescription
rowsreadonly T[] | State<readonly T[]>—The data, one object per row.
columnsGridColumn<T>[]—As in Table, plus filter, options, aggregate and format.
captionstring—Names the grid (required). It stays in the accessibility tree when captionHidden.
captionHiddenbooleanfalse
rowKeystring | ((row: T) => string)'id'
density"compact" | "cozy" | "comfortable"—
viewsRecord<string, GridView>—Named views for the View select.
viewstring—Which view it starts on.
groupBystring—The column the Group button groups by.
treeGridTree<T>—Hierarchical rows. An empty object uses row.kids.
filterstring—A tree's filter field, labelled this.
expand((row: T) => unknown)—What opens under a row.
bulk((rows: string[]) => unknown)—The bar's actions, given the keys of the rows in the range.
page{ size: number; }—Rows per page.
exportNamestring | false'export'The CSV's name. false removes Export.
actionsunknown—
maxHeightstring—
EventTypeDefaultDescription
onSaveView((name: string, view: GridView) => void)—
onEdit((row: T, column: string, value: unknown) => void)—
onPaste((changes: GridPaste<T>[]) => void)—A validated paste, as one change. Default each cell through onEdit.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="table"] [data-part="toolbar"]

toolbarclearspacerbulkviewportsr-onlysortresizefiltersloadingtree-celltree-labeltoggleskeletonaggregatefooter

RelatedTablePivot tableScroll grid