Stack
Puts things in a column with even space between them.
Preview
tsx
import { Grid, Stack, Text } from '@kerf-js/veneer'export function Gaps() { return ( <Grid min="xs" gap="group" > {STACK_GAPS.map((gap) => ( <Stack key={gap} gap="tight" > <Text role="label" subtle > {gap} </Text> <Stack gap={gap}> <Box>One</Box> <Box>Two</Box> <Box>Three</Box> </Stack> </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(--veneer-space-related);' + 'border:var(--veneer-border-hairline) ' + 'solid var(--veneer-color-border);' + 'border-radius:' + 'var(--veneer-radius-surface);' + 'background:var(--veneer-color-surface);' + 'color:var(--veneer-color-text);' + 'font-size:var(--veneer-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(--veneer-color-text-subtle)' } > {projectOf(children)!.region} </span> </> ) : ( children )} </div> )}const STACK_GAPS = [ 'hair', 'tight', 'related', 'group', 'section',] as constInstallation
npm install @kerf-js/veneerTry it
Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.
Live preview
Controls
Align
Codeupdates as you change the controls
import { Stack } from '@kerf-js/veneer'<Stack> <Box>First</Box> <Box>Second</Box> <Box>Third</Box></Stack>Usage
tsx
import { Stack } from '@kerf-js/veneer'Alignment
Without align children stretch to full width. With it they keep their own width.
tsx
import { Grid, Inset, Stack } from '@kerf-js/veneer'export function Align() { return ( <Grid min="xs" cols={3} gap="group" > {ALIGNS.map((align) => ( <Inset key={align}> <Stack gap="tight" align={align} > <Box>{align}</Box> <Box>A longer one</Box> <Box>Short</Box> </Stack> </Inset> ))} </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(--veneer-space-related);' + 'border:var(--veneer-border-hairline) ' + 'solid var(--veneer-color-border);' + 'border-radius:' + 'var(--veneer-radius-surface);' + 'background:var(--veneer-color-surface);' + 'color:var(--veneer-color-text);' + 'font-size:var(--veneer-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(--veneer-color-text-subtle)' } > {projectOf(children)!.region} </span> </> ) : ( children )} </div> )}const ALIGNS = ['start', 'center', 'end'] as constDivided
divided draws a hairline between children, with the gap on both sides of it.
tsx
import { Stack } from '@kerf-js/veneer'export function Divided() { return ( <Stack divided gap="related" > <div> <Box>One</Box> </div> <div> <Box>Two</Box> </div> <div> <Box>Three</Box> </div> </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(--veneer-space-related);' + 'border:var(--veneer-border-hairline) ' + 'solid var(--veneer-color-border);' + 'border-radius:' + 'var(--veneer-radius-surface);' + 'background:var(--veneer-color-surface);' + 'color:var(--veneer-color-text);' + 'font-size:var(--veneer-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(--veneer-color-text-subtle)' } > {projectOf(children)!.region} </span> </> ) : ( children )} </div> )}A settings group
Use divided to draw a line between the items in a group, and gap to set the space.
tsx
import { state } from '@kerf-js/core'import { Heading, Stack, Switch, Text} from '@kerf-js/veneer'export function Notifications() { const deploys = state(true) const failures = state(true) const digest = state(false) return ( <Stack gap="group"> <Stack gap="hair"> <Heading level={3}>Notifications</Heading> <Text subtle> Choose what kerf tells you about. </Text> </Stack> <Stack divided gap="related" > <Switch checked={deploys.value} onChange={() => (deploys.value = !deploys.value) } hint="When a deploy goes live." > Deploys </Switch> <Switch checked={failures.value} onChange={() => (failures.value = !failures.value) } hint="When a build or health check fails." > Failures </Switch> <Switch checked={digest.value} onChange={() => (digest.value = !digest.value)} hint="One email on Monday morning." > Weekly digest </Switch> </Stack> </Stack> )}A list of deploys
Use divided to separate each deploy from the next.
tsx
import { Badge, Row, Stack, Text } from '@kerf-js/veneer'export function DeployLog() { return ( <Stack divided gap="related" > {DEPLOYS.slice(0, 4).map((d) => ( <Row key={d.id} justify="between" gap="related" > <Stack gap="hair"> <Text>{d.message}</Text> <Text role="caption" subtle > {d.project} · {d.author} </Text> </Stack> <Badge tone={TONE_OF[d.status as keyof typeof TONE_OF]} dot > {d.status} </Badge> </Row> ))} </Stack> )}type Status = | 'Live' | 'Building' | 'Failed' | 'Paused'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', },]When to use
Accessibility
- Pass
as="ul"oras="ol"when the children are a list, andas="form"for a form.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
children | unknown | — | What it contains. |
as | "div" | "section" | "article" | "main" | "header" | "footer" | "nav" | "… | 'div' | The element: div by default, also form, ul, section and others. |
gap | "section" | "none" | "hair" | "tight" | "related" | "group" | "page" | — | The space between children, from the space scale. |
align | "start" | "center" | "end" | — | Where children sit across the column. Leave it out to stretch them. |
divided | boolean | false | Draws a hairline between children. |