Skip to content
weftGitHub

Grid

Even columns that fit themselves: as many as suit the room, up to the most you allow.

SourcePackage @weft-js/warpPlayground

Preview

tsx
import { Grid, Stack, Text } from '@weft-js/warp'export function MinSizes() {  return (    <Stack gap="group">      {MINS.map(min => (        <Stack key={min} gap="tight">          <Text role="label" subtle>min="{min}"</Text>          <Grid min={min} gap="tight">            {EIGHT.map(n => <Box key={n}>{n}</Box>)}          </Grid>        </Stack>      ))}    </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' },]const min = 60_000const BOX = 'display:grid;align-content:center;gap:2px;padding:var(--warp-space-related);border:var(--warp-border-hairline) solid var(--warp-color-border);border-radius:var(--warp-radius-surface);background:var(--warp-color-surface);color:var(--warp-color-text);font-size:var(--warp-text-caption-size);min-inline-size:0'const projectOf = (c: unknown) => {  const n = Number(typeof c === 'function' ? c() : c)  return Number.isInteger(n) && n > 0 ? PROJECTS[(n - 1) % PROJECTS.length] : undefined}function Box({ tall, children, ...rest }: { tall?: number; children?: unknown; [attr: string]: unknown }) {  return (    <div style={tall ? `${BOX};min-block-size:${tall}px` : BOX} {...rest}>      {projectOf(children) ? <><strong>{projectOf(children)!.name}</strong><span style="color:var(--warp-color-text-subtle)">{projectOf(children)!.region}</span></> : children}    </div>  )}const EIGHT = [1, 2, 3, 4, 5, 6, 7, 8]const MINS = ['xs', 'sm', 'md', 'lg'] as const

Installation

npm install @weft-js/warp

Usage

tsx
import { Grid } from '@weft-js/warp'<Stack gap="group">  {MINS.map(min => (    <Stack key={min} gap="tight">      <Text role="label" subtle>min="{min}"</Text>      <Grid min={min} gap="tight">        {EIGHT.map(n => <Box key={n}>{n}</Box>)}      </Grid>    </Stack>  ))}</Stack>

Most columns

cols is a ceiling, so a wide container stops at that many. min still wins when the room runs out: cols={6} with min="xs" needs about 810px, more than this stage has.

tsx
import { Grid, Stack, Text } from '@weft-js/warp'export function Cols() {  return (    <Stack gap="group">      {COLS.map(cols => (        <Stack key={cols} gap="tight">          <Text role="label" subtle>cols={'{'}{cols}{'}'}</Text>          <Grid min="xs" cols={cols} gap="tight">            {SIX.map(n => <Box key={n}>{n}</Box>)}          </Grid>        </Stack>      ))}    </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' },]const min = 60_000const BOX = 'display:grid;align-content:center;gap:2px;padding:var(--warp-space-related);border:var(--warp-border-hairline) solid var(--warp-color-border);border-radius:var(--warp-radius-surface);background:var(--warp-color-surface);color:var(--warp-color-text);font-size:var(--warp-text-caption-size);min-inline-size:0'const projectOf = (c: unknown) => {  const n = Number(typeof c === 'function' ? c() : c)  return Number.isInteger(n) && n > 0 ? PROJECTS[(n - 1) % PROJECTS.length] : undefined}function Box({ tall, children, ...rest }: { tall?: number; children?: unknown; [attr: string]: unknown }) {  return (    <div style={tall ? `${BOX};min-block-size:${tall}px` : BOX} {...rest}>      {projectOf(children) ? <><strong>{projectOf(children)!.name}</strong><span style="color:var(--warp-color-text-subtle)">{projectOf(children)!.region}</span></> : children}    </div>  )}const SIX = [1, 2, 3, 4, 5, 6]const COLS = [2, 3, 4] as const

A full-width child

Put data-span="full" on a direct child and it takes the whole row.

tsx
import { Grid } from '@weft-js/warp'export function Span() {  return (    <Grid min="xs" cols={4} gap="related">      <Box data-span="full">data-span="full"</Box>      {FOUR.map(n => <Box key={n} tall={72}>{n}</Box>)}      <Box data-span="full">data-span="full"</Box>    </Grid>  )}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 min = 60_000const BOX = 'display:grid;align-content:center;gap:2px;padding:var(--warp-space-related);border:var(--warp-border-hairline) solid var(--warp-color-border);border-radius:var(--warp-radius-surface);background:var(--warp-color-surface);color:var(--warp-color-text);font-size:var(--warp-text-caption-size);min-inline-size:0'const projectOf = (c: unknown) => {  const n = Number(typeof c === 'function' ? c() : c)  return Number.isInteger(n) && n > 0 ? PROJECTS[(n - 1) % PROJECTS.length] : undefined}function Box({ tall, children, ...rest }: { tall?: number; children?: unknown; [attr: string]: unknown }) {  return (    <div style={tall ? `${BOX};min-block-size:${tall}px` : BOX} {...rest}>      {projectOf(children) ? <><strong>{projectOf(children)!.name}</strong><span style="color:var(--warp-color-text-subtle)">{projectOf(children)!.region}</span></> : children}    </div>  )}const FOUR = [1, 2, 3, 4]

Gap

gap comes from the space scale, so it says how related the items are rather than a number of pixels.

tsx
import { Grid, Stack, Text } from '@weft-js/warp'export function Gaps() {  return (    <Grid min="sm" cols={2} gap="section">      {GAPS.map(gap => (        <Stack key={gap} gap="tight">          <Text role="label" subtle>gap="{gap}"</Text>          <Grid min="xs" cols={3} gap={gap}>            {SIX.map(n => <Box key={n}>{n}</Box>)}          </Grid>        </Stack>      ))}    </Grid>  )}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 min = 60_000const BOX = 'display:grid;align-content:center;gap:2px;padding:var(--warp-space-related);border:var(--warp-border-hairline) solid var(--warp-color-border);border-radius:var(--warp-radius-surface);background:var(--warp-color-surface);color:var(--warp-color-text);font-size:var(--warp-text-caption-size);min-inline-size:0'const projectOf = (c: unknown) => {  const n = Number(typeof c === 'function' ? c() : c)  return Number.isInteger(n) && n > 0 ? PROJECTS[(n - 1) % PROJECTS.length] : undefined}function Box({ tall, children, ...rest }: { tall?: number; children?: unknown; [attr: string]: unknown }) {  return (    <div style={tall ? `${BOX};min-block-size:${tall}px` : BOX} {...rest}>      {projectOf(children) ? <><strong>{projectOf(children)!.name}</strong><span style="color:var(--warp-color-text-subtle)">{projectOf(children)!.region}</span></> : children}    </div>  )}const SIX = [1, 2, 3, 4, 5, 6]const GAPS = ['tight', 'related', 'group', 'section'] as const

A dashboard

Four stats, with a full-width card under them.

tsx
import { Card, Grid, Progress, Stack, Text } from '@weft-js/warp'export function Dashboard() {  return (    <Grid min="xs" cols={4} gap="related">      {STATS.map(s => (        <Card key={s.label}>          <Stack gap="hair">            <Text role="label" subtle>{s.label}</Text>            <Text role="title">{s.value}</Text>            <Text role="caption" tone={s.good ? 'positive' : 'caution'}>{s.delta} on September</Text>          </Stack>        </Card>      ))}      <Card data-span="full" title="Traffic by region">        <Grid min="sm" gap="related">          {REGIONS.map(r => <Progress key={r.region} label={r.region} value={r.share} />)}        </Grid>      </Card>    </Grid>  )}const TRAFFIC = [  { month: 'Nov', requests: 41.2, errors: 0.42, deploys: 88, cost: 6120 },  { month: 'Dec', requests: 52.8, errors: 0.51, deploys: 74, cost: 6480 },  { month: 'Jan', requests: 47.1, errors: 0.38, deploys: 96, cost: 6390 },  { month: 'Feb', requests: 49.6, errors: 0.33, deploys: 104, cost: 6710 },  { month: 'Mar', requests: 58.3, errors: 0.47, deploys: 118, cost: 7240 },  { month: 'Apr', requests: 61.9, errors: 0.29, deploys: 126, cost: 7380 },  { month: 'May', requests: 66.4, errors: 0.31, deploys: 131, cost: 7690 },  { month: 'Jun', requests: 64.2, errors: 0.44, deploys: 122, cost: 7620 },  { month: 'Jul', requests: 71.5, errors: 0.27, deploys: 139, cost: 8010 },  { month: 'Aug', requests: 74.8, errors: 0.25, deploys: 144, cost: 8230 },  { month: 'Sep', requests: 79.3, errors: 0.36, deploys: 152, cost: 8560 },  { month: 'Oct', requests: 86.1, errors: 0.22, deploys: 167, cost: 8930 },]const REGIONS = [  { region: 'eu-west', requests: 34.2, share: 40 },  { region: 'us-east', requests: 24.8, share: 29 },  { region: 'ap-south', requests: 12.1, share: 14 },  { region: 'us-west', requests: 9.4, share: 11 },  { region: 'eu-north', requests: 5.6, share: 6 },]const min = 60_000const now = TRAFFIC[11]!, before = TRAFFIC[10]!const change = (a: number, b: number) => `${a >= b ? '+' : ''}${((a - b) / b * 100).toFixed(1)}%`const STATS = [  { label: 'Requests', value: `${now.requests}M`, delta: change(now.requests, before.requests), good: true },  { label: 'Error rate', value: `${now.errors}%`, delta: change(now.errors, before.errors), good: true },  { label: 'Deploys', value: String(now.deploys), delta: change(now.deploys, before.deploys), good: true },  { label: 'Spend', value: `$${now.cost.toLocaleString('en-US')}`, delta: change(now.cost, before.cost), good: false },]

Project cards

Use min for the narrowest a card may get and cols for the most that sit across.

tsx
import { Badge, Card, Grid, Row, Stack, Text } from '@weft-js/warp'export function ProjectCards() {  return (    <Grid min="sm" cols={3} gap="related">      {PROJECTS.slice(0, 6).map(p => (        <Card key={p.id}>          <Stack gap="tight">            <Row justify="between" gap="tight">              <Text role="heading">{p.name}</Text>              <Badge tone={TONE_OF[p.status]} dot>{p.status}</Badge>            </Row>            <Text subtle>{p.owner}</Text>            <Text role="caption" subtle>{p.region} · {p.deploys} deploys · {p.latency} ms</Text>          </Stack>        </Card>      ))}    </Grid>  )}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 min = 60_000

When to use

Use it for

  • Cards, tiles or stats of equal weight
  • A set of items that should reflow without breakpoints

Reach for something else

  • Split, when one side is fixed and the other fluid
  • Row, when the items keep their own widths
  • Table, when the data has columns that mean something

Playground

Min
Cols
import { Grid } from '@weft-js/warp'<Grid min="sm">  <Box>1</Box>  <Box>2</Box>  <Box>3</Box></Grid>

Accessibility

  • Grid changes how things look, not their order. Reading and tab order follow the source.
  • Pass as="ul" for a real list of items.

API Reference

PropTypeDefaultDescription
childrenunknown—What it contains.
as"div" | "section" | "article" | "main" | "header" | "footer" | "nav" | "…'div'The element: div by default, also ul, section and others.
gap"section" | "none" | "hair" | "tight" | "related" | "group" | "page"—The space between items, from the space scale.
min"xs" | "sm" | "md" | "lg"—The narrowest a column may get: xs 8rem, sm 11rem, md 16rem, lg 22rem.
cols2 | 3 | 4 | 6—The most columns it will use, however wide it gets.

RelatedSplitStackRowCard