Row
Puts things side by side and wraps them when they run out of room.
Preview
tsx
import { Inset, Row, Stack } from '@kerf-js/veneer'export function Justify() { return ( <Stack gap="related"> {JUSTIFIES.map((justify) => ( <Inset key={justify}> <Row justify={justify} gap="tight" > <Box>{justify}</Box> <Box>Two</Box> <Box>Three</Box> </Row> </Inset> ))} </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> )}const JUSTIFIES = [ 'start', 'center', 'end', 'between',] 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
Justify
Align
Codeupdates as you change the controls
import { Row } from '@kerf-js/veneer'<Row> <Box>Short</Box> <Box>Tall</Box> <Box>A little wider</Box></Row>Usage
tsx
import { Row } from '@kerf-js/veneer'Alignment
Items centre by default. start tops them, baseline lines up text.
tsx
import { Grid, Heading, Inset, Row, Text} from '@kerf-js/veneer'export function Align() { return ( <Grid min="xs" cols={3} gap="group" > <Inset> <Row gap="tight"> <Box tall={72}>Tall</Box> <Box>center</Box> </Row> </Inset> <Inset> <Row gap="tight" align="start" > <Box tall={72}>Tall</Box> <Box>start</Box> </Row> </Inset> <Inset> <Row gap="tight" align="baseline" > <Heading level={3}>Heading</Heading> <Text subtle>baseline</Text> </Row> </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> )}Wrapping
It wraps by itself. wrap={false} keeps it on one line.
tsx
import { Row } from '@kerf-js/veneer'export function Wraps() { return ( <Row gap="tight"> {TAGS.map((t) => ( <Box key={t}>{t}</Box> ))} </Row> )}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 TAGS = [ 'eu-west', 'us-east', 'ap-south', 'us-west', 'eu-north', 'global', 'staging', 'preview', 'canary', 'archive',]A child that grows
data-grow on a child takes the free space.
tsx
import { Row } from '@kerf-js/veneer'export function Grow() { return ( <Row gap="related" wrap={false} > <Box>Fixed</Box> <Box data-grow="">data-grow takes the rest</Box> <Box>Fixed</Box> </Row> )}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> )}Reflow
reflow turns it into a stack when its container is narrow. Set the stage's Width to Tablet and Phone to see each step go.
tsx
import { Row, Stack, Text } from '@kerf-js/veneer'export function Reflow() { return ( <Stack gap="group"> <Stack gap="tight"> <Text role="label" subtle > reflow="tablet": a stack below 600px </Text> <Row gap="tight" reflow="tablet" > <Box>One</Box> <Box>Two</Box> <Box>Three</Box> </Row> </Stack> <Stack gap="tight"> <Text role="label" subtle > reflow="desktop": a stack below 1000px </Text> <Row gap="tight" reflow="desktop" > <Box>One</Box> <Box>Two</Box> <Box>Three</Box> </Row> </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(--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 page header
Title and actions apart, stacked when there is no room for both.
tsx
import { Button, Heading, Row, Stack, Text} from '@kerf-js/veneer'export function PageHeader() { return ( <Row justify="between" gap="group" reflow="tablet" > <Stack gap="hair"> <Heading level={3} role="title" > storefront </Heading> <Text subtle> Live in eu-west. Last deploy 2 hours ago. </Text> </Stack> <Row gap="tight"> <Button emphasis="outline" tone="neutral" icon="terminal" > Logs </Button> <Button icon="rocket">Deploy</Button> </Row> </Row> )}When to use
Accessibility
- This has no shortcuts of its own: controls inside it use Tab, Enter and Space as usual, and veneer's parts keep their roles and labels.
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 header, footer, nav and others. |
gap | "section" | "none" | "hair" | "tight" | "related" | "group" | "page" | — | The space between items, from the space scale. |
justify | "start" | "center" | "end" | "between" | — | Where the items sit along the row. |
align | "start" | "baseline" | — | How the items line up across the row. Centred by default. |
wrap | boolean | — | false keeps it on one line. |
reflow | Step | — | Below this step of its container it becomes a stack: tablet is 600px, desktop 1000px. |