Lay a screen's parts out in a column, a row or a grid, spaced by the library's spacing steps instead of pixel numbers (flex and grid layout). Reach for them before a wrapper with margins of its own.
import { Stack, Grid } from "@umriss-ui/core";
A gap is a step of the 4 px scale from 1 to 8, the same steps every component spaces itself by, so a screen keeps its proportions. align and justify pass alignItems and justifyContent through unchanged; for the rest of flexbox and grid, set style.
Stack parts below one another and set the space between them with gap, a step of the 4 px scale.
import { Stack, Text } from "@umriss-ui/core";
export default function AColumn() {
return (
<Stack gap={2}>
<Text size="xs" tone="muted">
INC-1048 · SEV1 · opened 09:42
</Text>
<Text weight="medium">Checkout slow, card payments time out</Text>
<Text size="sm" tone="secondary">
Assigned to Jonas Keller, acknowledged at 09:46.
</Text>
</Stack>
);
}
Set direction to row for parts beside one another; align and justify pass straight through to flexbox.
import { Badge, Button, Stack, Text } from "@umriss-ui/core";
export default function ARow() {
return (
<Stack gap={4}>
<Stack direction="row" gap={2} align="center">
<Button size="sm" variant="primary">
Acknowledge
</Button>
<Button size="sm">Assign</Button>
<Button size="sm" variant="ghost">
Resolve
</Button>
</Stack>
<Stack direction="row" gap={3} align="center" justify="space-between">
<Text weight="medium">Checkout</Text>
<Badge tone="danger">SEV1 open</Badge>
</Stack>
</Stack>
);
}
Pick the smallest gap that still separates: 1 or 2 inside a group, 4 to 6 between groups.
import { Badge, Stack, Text } from "@umriss-ui/core";
const TEAMS = ["Payments", "Identity", "Discovery"];
export default function Gaps() {
return (
<Stack gap={4}>
{([1, 3, 6] as const).map((gap) => (
<Stack key={gap} direction="row" gap={gap} align="center">
<Text size="xs" tone="muted" style={{ width: 48, flex: "none", whiteSpace: "nowrap" }}>
gap {gap}
</Text>
<Stack direction="row" gap={gap} align="center" wrap>
{TEAMS.map((team) => (
<Badge key={team}>{team}</Badge>
))}
</Stack>
</Stack>
))}
</Stack>
);
}
Set wrap where a row may hold more than fits, so a long run of labels breaks onto the next line.
import { Card, CardBody, Stack, Tag } from "@umriss-ui/core";
const SERVICES = ["Checkout", "Billing", "Sign-in", "Search", "Image service", "Notifications", "Webhooks", "Reporting"];
export default function Wrapping() {
return (
<Card style={{ maxWidth: 320 }}>
<CardBody>
<Stack direction="row" gap={2} wrap>
{SERVICES.map((name) => (
<Tag key={name}>{name}</Tag>
))}
</Stack>
</CardBody>
</Card>
);
}
Use columns where the layout decides the count, such as three figures that always stand side by side.
import { Card, CardBody, Grid, Stack, Text } from "@umriss-ui/core";
const FIGURES = [
{ label: "Marketing budget, March", value: "68,000\u00a0€" },
{ label: "Forecast, March", value: "77,500\u00a0€" },
{ label: "Over budget", value: "9,500\u00a0€" },
];
export default function FixedColumns() {
return (
<Grid columns={3} gap={3}>
{FIGURES.map((figure) => (
<Card key={figure.label}>
<CardBody>
<Stack gap={1}>
<Text size="xs" tone="muted">
{figure.label}
</Text>
<Text weight="medium">{figure.value}</Text>
</Stack>
</CardBody>
</Card>
))}
</Grid>
);
}
Set minItemWidth instead and the grid fits as many columns as stay readable; it wins over columns.
import { Card, CardBody, Grid, Stack, Text } from "@umriss-ui/core";
/* Data from the controlling world, written out here so the example runs on its own. */
interface CostCentre {
id: string;
name: string;
owner: string;
/** The budget of one month. */
monthlyBudget: number;
}
const COST_CENTRES: readonly CostCentre[] = [
{ id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 },
{ id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 },
{ id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 },
{ id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 },
{ id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 },
{ id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 },
{ id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 },
{ id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 },
{ id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 },
];
export default function ColumnsByWidth() {
return (
<Grid minItemWidth="200px" gap={3}>
{COST_CENTRES.map((centre) => (
<Card key={centre.id}>
<CardBody>
<Stack gap={1}>
<Text size="xs" tone="muted">
{centre.id} · {centre.owner}
</Text>
<Text weight="medium">{centre.name}</Text>
<Text size="sm" tone="secondary">
{centre.monthlyBudget.toLocaleString("en")} € a month
</Text>
</Stack>
</CardBody>
</Card>
))}
</Grid>
);
}
import { Card, CardBody, Grid, Text } from "@umriss-ui/core";
/* One cost centre's key figures, written out here so the example runs on its own. */
const FIGURES = [
{ label: "Cost centre", value: "CC-1200 · Marketing" },
{ label: "Owner", value: "Rafael Ortiz" },
{ label: "Monthly budget", value: "68,000\u00a0€" },
{ label: "Spent, March", value: "77,500\u00a0€, 9,500\u00a0€ over budget" },
{ label: "Approved by", value: "Helen Marsh, Sales" },
];
export default function ColumnsOfTheirOwnWidths() {
return (
<Card style={{ maxWidth: 480 }}>
<CardBody>
<Grid columns={[140, "fill"]} gap={3}>
{FIGURES.flatMap((figure) => [
<Text key={`${figure.label} label`} size="sm" tone="muted">
{figure.label}
</Text>,
<Text key={`${figure.label} value`} size="sm">
{figure.value}
</Text>,
])}
</Grid>
</CardBody>
</Card>
);
}
Stacks and grids nest: a column holds a header row, a grid of key figures and a row of actions.
import { Badge, Button, Card, CardBody, Divider, Grid, Heading, Stack, Text } from "@umriss-ui/core";
const FIGURES = [
{ label: "Vehicle", value: "FP 214 K · van" },
{ label: "Driver", value: "Martin Hale" },
{ label: "Stops", value: "11, two late" },
{ label: "Distance", value: "86 km" },
{ label: "Leaves", value: "07:00, North depot" },
{ label: "Back", value: "13:40" },
];
export default function ATourSummary() {
return (
<Card style={{ maxWidth: 640 }}>
<CardBody>
<Stack gap={4}>
<Stack direction="row" gap={3} align="center" justify="space-between">
<Heading level={3} size="lg">
Tour T-01
</Heading>
<Badge tone="warning">2 stops late</Badge>
</Stack>
<Grid minItemWidth="160px" gap={4}>
{FIGURES.map((figure) => (
<Stack key={figure.label} gap={1}>
<Text size="xs" tone="muted">
{figure.label}
</Text>
<Text size="sm">{figure.value}</Text>
</Stack>
))}
</Grid>
<Divider />
<Stack direction="row" gap={2} justify="flex-end">
<Button size="sm" variant="ghost">
Call the driver
</Button>
<Button size="sm" variant="primary">
Reorder stops
</Button>
</Stack>
</Stack>
</CardBody>
</Card>
);
}
The keys of Button apply here.
StackProps| Name | Type | Default | Description |
|---|---|---|---|
direction | "row" | "column" | "column" | Below one another or beside one another. Below is the direction content stands in anyway, without being asked. |
gap | SpaceStep| 1 | 3 | Spacing as a step of the 4 px spacing steps (1–8). |
align | CSSProperties["alignItems"] | — | Alignment across the direction – alignItems unchanged.Shown in: A row, Gaps, A tour summary and 78 more |
justify | CSSProperties["justifyContent"] | — | Distribution along the direction – justifyContent unchanged.Shown in: A row, A tour summary, Name the same action in every row (VisuallyHidden) and 9 more |
wrap | boolean | false | Lets the children wrap when the line does not suffice. Shown in: Gaps, Wrapping, Sized to the value (Sizes) and 32 more |
ref | Ref<HTMLDivElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
Also takes every attribute of <div>.
GridProps| Name | Type | Default | Description |
|---|---|---|---|
columns | number | readonly GridColumnWidth[] | 2 | How many columns, all of one width - or a list with one width per
column: a number in pixels, or "fill" for a share of what is left.
Overridden by minItemWidth.Shown in: Fixed columns, Columns of their own widths, A token for one region (Theming) and 5 more |
minItemWidth | string | — | Responsive: as many columns as fit at this minimum width; wins over
columns, a list included.Shown in: Columns by width, A tour summary, A dark region (Theming) and 51 more |
gap | SpaceStep| 1 | 4 | Spacing as a step of the 4 px spacing steps (1–8); applies in both
directions. Shown in: Fixed columns, Columns by width, Columns of their own widths and 58 more |
ref | Ref<HTMLDivElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
Also takes every attribute of <div>.
SpaceStepA step of the spacing scale, from 1 (4 px) to 8 (40 px) - the tokens
--u-space-1 to --u-space-8.
type SpaceStep = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;GridColumnWidthThe width of one column in Grid's list: a number in pixels, or
"fill" for a share of what the other columns leave.
type GridColumnWidth = number | "fill";minItemWidth, anything else is your stylesheet's.@umriss-ui/dashboard (ADR-0064).What umriss deliberately does not build, and why: ADR-0032.