Skip to content
weftGitHub

Table

Rows of data that people can sort, filter, select and page through.

SourcePackage @weft-js/warpPlayground

Preview

tsx
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/warp

Usage

tsx
import { Table } from '@weft-js/warp'

Filtering

filter adds the field and searches every column. Try "eu-west" or "Lena".

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
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) % 260

A fixed height

maxHeight scrolls the rows under a sticky header.

tsx
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.

tsx
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) % 260

Live numbers

A live cell flashes up or down as its value changes. Only that cell updates.

tsx
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) % 260

Loading

Comes by itself with a Resource. status forces it.

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
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

Density
Phone
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
  • caption is required: it names the table, and captionHidden keeps 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

PropTypeDefaultDescription
rowsreadonly 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.
captionstring—Names the table. It stays in the accessibility tree when captionHidden.
captionHiddenbooleanfalseHides the caption on screen. Screen readers still hear it.
columnsColumn<T>[]—What each column shows. See id, header, sort, cell, numeric, priority, edit in the examples.
rowKeystring | ((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.
filterstring—Show the filter field, labelled this.
queryState<string>—A state holding the filter text, so you can read or set it.
sortState<TableSort | undefined>—A state holding the sorted column and direction, so you can read or set it.
serverbooleanfalseThe rows arrive sorted, filtered and paged, so the table only reports what was asked.
selectionboolean | 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.
actionsunknown—The toolbar's own actions.
pickerbooleantrueThe Columns button. On by default.
hiddenColumnsreadonly string[] | State<readonly string[]>—Columns hidden from the start, in the server's HTML too. Bind a State to keep what people choose.
statusTableStatus—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.
maxHeightstring—A CSS height past which the body scrolls under a sticky header, such as 480px.
headingnumber2The outline level of the table's state and sheet headings, under the page's h1.
EventTypeDefaultDescription
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

RelatedData gridPagination barVirtual list