Stack and Grid

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.

Examples

A column

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>
  );
}

A row

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>
  );
}

Gaps

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>
  );
}

Wrapping

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>
  );
}

Fixed columns

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>
  );
}

Columns by width

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>
  );
}

Columns of their own widths

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>
  );
}

A tour summary

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>
  );
}

Keyboard

The keys of Button apply here.

API

StackProps

NameTypeDefaultDescription
direction"row" | "column""column"Below one another or beside one another. Below is the direction content stands in anyway, without being asked.

Shown in: A row, Gaps, Wrapping and 69 more

gapSpaceStep
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
3Spacing as a step of the 4 px spacing steps (1–8).

Shown in: A column, A row, Gaps and 164 more

alignCSSProperties["alignItems"]—Alignment across the direction – alignItems unchanged.

Shown in: A row, Gaps, A tour summary and 78 more

justifyCSSProperties["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

wrapbooleanfalseLets the children wrap when the line does not suffice.

Shown in: Gaps, Wrapping, Sized to the value (Sizes) and 32 more

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown 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

NameTypeDefaultDescription
columnsnumber | readonly GridColumnWidth[]2How 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

minItemWidthstring—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

gapSpaceStep
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
4Spacing 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

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

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

Types on this page

SpaceStep

A 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;

GridColumnWidth

The 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";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index