Table
Rows of data that people can sort, filter, select and page through.
SourcePackage @weft-js/warpPlayground
Preview
import { state } from '@weft-js/core'import { Badge, Stack, Table, Text } from '@weft-js/warp'import type { Column, KanbanCardData, TableSort } from '@weft-js/warp'export function Sortable() { const sort = state<TableSort | undefined>({ id: 'cost', dir: 'descending' }) return ( <Stack gap="related"> <Text subtle>Sorted by {sort.value?.id}, {sort.value?.dir}.</Text> <Table caption="Projects by cost" captionHidden rows={PROJECTS} columns={COLUMNS} sort={sort} picker={false} /> </Stack> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Installation
npm install @weft-js/warpUsage
import { Table } from '@weft-js/warp'Filtering
filter adds the field and searches every column. Try "eu-west" or "Lena".
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Filter() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={COLUMNS} filter="Filter projects" picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Selection and bulk actions
Shift-click selects a range. bulk fills the bar, and these buttons really pause and delete.
import { state } from '@weft-js/core'import { Badge, Button, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Select() { const rows = state(PROJECTS) const picked = state<Record<string, boolean>>({}) const pause = (keys: string[]) => { rows.value = rows.value.map(p => (keys.includes(p.id) ? { ...p, status: 'Paused' as const } : p)) picked.value = {} } const remove = (keys: string[]) => { rows.value = rows.value.filter(p => !keys.includes(p.id)) picked.value = {} } return ( <Table caption="Projects" captionHidden rows={rows} columns={COLUMNS} picker={false} selection={picked} bulk={keys => ( <> <Button size="sm" emphasis="outline" tone="neutral" icon="pause" onClick={() => pause(keys)}>Pause</Button> <Button size="sm" emphasis="outline" tone="critical" icon="trash" onClick={() => remove(keys)}>Delete</Button> </> )} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Pages
page slices the rows and adds the footer.
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Paged() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={COLUMNS} page={FIVE} picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]const FIVE = { size: 5 }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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Row details
Press a row. expand draws what opens.
import { Badge, Row, Stack, Table, Text } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Details() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={COLUMNS} expand={details} picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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>)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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Columns that give way
priority 2 hides below 760px of table width and 3 below 1000px. Hidden fields move into the row's details.
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Priority() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={RANKED} picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const RANKED: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, card: 'title' }, { id: 'status', header: 'Status', card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', priority: 2 }, { id: 'region', header: 'Region', priority: 2 }, { id: 'latency', header: 'Latency', numeric: true, priority: 3 }, { id: 'uptime', header: 'Uptime', numeric: true, priority: 3 },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Pinned and resizable columns
pin keeps a column in place while the rest scroll. Drag a header's edge to resize it.
import { Badge, Table } from '@weft-js/warp'import type { Column } from '@weft-js/warp'export function Pinned() { return ( <Table caption="Every field of every project" captionHidden rows={PROJECTS} columns={WIDE} phone="columns" /> )}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 WIDE: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, pin: true, resize: true }, { id: 'status', header: 'Status', resize: true, cell: badge }, { id: 'owner', header: 'Owner', resize: true }, { id: 'region', header: 'Region' }, { id: 'deploys', header: 'Deploys', numeric: true }, { id: 'uptime', header: 'Uptime', numeric: true }, { id: 'latency', header: 'Latency', numeric: true }, { id: 'cost', header: 'Cost', numeric: true, cell: p => money(p.cost) }, { id: 'updated', header: 'Updated' },]const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260A fixed height
maxHeight scrolls the rows under a sticky header.
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Sticky() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={COLUMNS} maxHeight="280px" picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Inline editing
edit turns text into the value, validate refuses a bad one, and onEdit saves it.
import { state } from '@weft-js/core'import { Stack, Table, Text } from '@weft-js/warp'import type { Column } from '@weft-js/warp'export function Edit() { const rows = state(PROJECTS.slice(0, 5)) const last = state('Double-click a cell, or focus it and press Enter.') const save = (row: Project, column: string, value: unknown) => { rows.value = rows.value.map(p => (p.id === row.id ? { ...p, [column]: value } : p)) last.value = `Saved ${column} for ${row.name}.` } return ( <Stack gap="related"> <Text subtle>{last.value}</Text> <Table caption="Budgets" captionHidden rows={rows} columns={PRICED} onEdit={save} picker={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 PRICED: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, edit: true, validate: v => String(v).trim() !== '' || 'A project needs a name' }, { id: 'owner', header: 'Owner', edit: true }, { id: 'cost', header: 'Budget', numeric: true, cell: p => money(p.cost), 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) % 260Live numbers
A live cell flashes up or down as its value changes. Only that cell updates.
import { onCleanup, onMount, state } from '@weft-js/core'import { Badge, Table } from '@weft-js/warp'import type { Column } from '@weft-js/warp'export function Live() { const ms = state(Object.fromEntries(WATCHED.map(p => [p.id, p.latency]))) const columns: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true }, { id: 'status', header: 'Status', cell: badge }, { id: 'latency', header: 'Latency (ms)', numeric: true, live: true, value: p => ms.get(p.id) }, ] const tick = () => { const next = { ...ms.value } for (const id in next) next[id] = Math.max(12, next[id]! + Math.round(Math.random() * 24 - 12)) ms.value = next } onMount(() => { const timer = setInterval(tick, 1200) onCleanup(() => clearInterval(timer)) }) return <Table caption="Latency now" captionHidden rows={WATCHED} columns={columns} picker={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 badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>const WATCHED = PROJECTS.slice(0, 5)const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260Loading
Comes by itself with a Resource. status forces it.
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Loading() { return ( <Table caption="Projects" captionHidden rows={PROJECTS} columns={COLUMNS} status="loading" picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Empty
Use empty to say what to show when there are no rows.
import { Badge, Button, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Empty() { return ( <Table caption="Projects" captionHidden rows={NONE} columns={COLUMNS} picker={false} empty={{ title: 'No projects yet', text: 'Connect a repository and your first deploy runs in about a minute.', action: <Button icon="plus">New project</Button> }} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]const NONE: Project[] = []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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Error
Retry calls onRetry, and here it succeeds.
import { state } from '@weft-js/core'import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Error() { const failed = state(true) return <Table caption="Projects" captionHidden rows={WATCHED} columns={COLUMNS} picker={false} status={failed.value ? 'error' : undefined} error={{ title: 'Projects could not load', text: 'The API timed out. Nothing was lost.' }} onRetry={() => (failed.value = false)} />}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 COLUMNS: Column<Project>[] = [ { id: 'name', header: 'Project', rowHeader: true, sort: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'owner', header: 'Owner', sort: true }, { id: 'region', header: 'Region', sort: true }, { id: 'deploys', header: 'Deploys', numeric: true, sort: true }, { id: 'cost', header: 'Cost', numeric: true, sort: true, cell: p => money(p.cost) },]const WATCHED = PROJECTS.slice(0, 5)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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Recent deploys
Use filter to add a search box, density to set how tight the rows sit, and actions for buttons above the table.
import { Badge, Button, Table, Text } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Deploys() { return ( <Table caption="Recent deploys" rows={DEPLOYS} columns={DEPLOY_COLUMNS} filter="Filter deploys" density="cozy" actions={<Button icon="rocket">Deploy</Button>} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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' }const DEPLOYS = [ { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' }, { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' }, { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' }, { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' }, { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' }, { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]type Project = (typeof PROJECTS)[number]const badge = (p: { status: string }) => <Badge tone={TONE_OF[p.status as Status]} dot>{p.status}</Badge>type Deploy = (typeof DEPLOYS)[number]const DEPLOY_COLUMNS: Column<Deploy>[] = [ { id: 'message', header: 'Change', rowHeader: true, card: 'title' }, { id: 'status', header: 'Status', sort: true, card: 'badge', cell: badge }, { id: 'project', header: 'Project', sort: true }, { id: 'author', header: 'Author', sort: true, priority: 2 }, { id: 'commit', header: 'Commit', priority: 2, cell: d => <Text inline role="code">{d.commit}</Text> }, { id: 'at', header: 'When', sort: true, value: d => d.at.replace('T', ' ') },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })Team members
Use selection to let people tick rows, and sort on a column to let them order by it.
import { Badge, Table } from '@weft-js/warp'import type { Column, KanbanCardData } from '@weft-js/warp'export function Team() { return ( <Table caption="Team" rows={PEOPLE} columns={TEAM_COLUMNS} filter="Find a person" selection picker={false} /> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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]type Person = (typeof PEOPLE)[number]const TEAM_COLUMNS: Column<Person>[] = [ { id: 'name', header: 'Name', rowHeader: true, sort: true, card: 'title' }, { id: 'email', header: 'Email', card: 'hide' }, { id: 'team', header: 'Team', sort: true }, { id: 'role', header: 'Role', sort: true, card: 'badge', cell: p => <Badge tone={p.role === 'Owner' ? 'accent' : 'neutral'}>{p.role}</Badge> },]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 latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })When to use
Use it for
- Records people scan and compare
- Lists that need sorting, filtering or bulk actions
Reach for something else
- DataGrid, when people work cell by cell, like a spreadsheet
- VirtualList, when there are many thousands of rows
- ScrollGrid, when there are many columns as well as rows
Playground
import { Table } from '@weft-js/warp'<Table caption="Projects" rows={projects} columns={columns} filter="Filter projects" />Accessibility
- Tab
- Moves through the headers, checkboxes and row buttons
- Enter
- On a header, sorts. On an editable cell, edits
- Esc
- Cancels an edit
- Shiftclick
- Selects a range of rows
captionis required: it names the table, andcaptionHiddenkeeps it for screen readers only.- Below 600px of table width rows become cards, unless
phone="columns". Roles stay on the elements so it still reads as a table.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
rows | readonly T[] | State<readonly T[]> | Derived<readonly T[]> | Resource<re… | — | A list, a state, or a Resource. Given a list it sorts, filters and pages by itself. |
caption | string | — | Names the table. It stays in the accessibility tree when captionHidden. |
captionHidden | boolean | false | Hides the caption on screen. Screen readers still hear it. |
columns | Column<T>[] | — | What each column shows. See id, header, sort, cell, numeric, priority, edit in the examples. |
rowKey | string | ((row: T) => string) | 'id' | The field that identifies a row, or how to get it. Default id. |
phone | "columns" | "cards" | 'cards' | Below 600px: cards, or columns to keep scrolling sideways. |
density | "compact" | "cozy" | "comfortable" | — | How tight the rows are. |
filter | string | — | Show the filter field, labelled this. |
query | State<string> | — | A state holding the filter text, so you can read or set it. |
sort | State<TableSort | undefined> | — | A state holding the sorted column and direction, so you can read or set it. |
server | boolean | false | The rows arrive sorted, filtered and paged, so the table only reports what was asked. |
selection | boolean | State<Record<string, boolean>> | — | true, or a state to hold the picked keys yourself. |
bulk | ((keys: string[]) => unknown) | — | The bulk bar's actions. |
page | { size: number; } | — | Rows per page. |
expand | ((row: T) => unknown) | — | What opens under a row. Default a list of every column. |
details | "sheet" | "inline" | 'inline' | sheet opens details in a panel below 1000px. |
actions | unknown | — | The toolbar's own actions. |
picker | boolean | true | The Columns button. On by default. |
hiddenColumns | readonly string[] | State<readonly string[]> | — | Columns hidden from the start, in the server's HTML too. Bind a State to keep what people choose. |
status | TableStatus | — | Forces a state; a Resource's own is used otherwise. |
empty | { title?: unknown; text?: unknown; action?: unknown; } | — | What to show when there are no rows: a title, a line of text and an action. |
error | { title?: unknown; text?: unknown; } | — | A message that marks the field invalid and says why. |
maxHeight | string | — | A CSS height past which the body scrolls under a sticky header, such as 480px. |
heading | number | 2 | The outline level of the table's state and sheet headings, under the page's h1. |
| Event | Type | Default | Description |
|---|---|---|---|
onSortChange | ((sort: TableSort) => void) | — | Called with the column and direction when someone sorts. |
onSelectionChange | ((keys: string[]) => void) | — | Called with the keys of every selected row after each change. |
onPageChange | ((to: unknown, reason: "next" | "prev") => void) | — | Called on Next or Previous, with the page or server cursor to load. |
onEdit | ((row: T, column: string, value: unknown) => void) | — | Called with the row, the column's id and the new value when a cell edit is committed. |
onRetry | (() => void) | — | Called by the error state's Retry. A Resource refreshes itself as well. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="table"] [data-part="hide"]
hidetoolbarcountspacerbulksortbarviewportselectsr-onlysortresizeskeletonstatefooter