# Stack and Grid

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

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.

```ts
import { Stack, Grid } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/

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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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`.

```tsx
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

```tsx
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.

```tsx
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](https://romanhaendler.github.io/umriss-ui/core/button/#keyboard-button) apply here.

## API

### `StackProps`

| Prop | 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.<br>Shown in: [A row](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-row), [Gaps](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#gaps), [Wrapping](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#wrapping) and 69 more |
| `gap` | [`SpaceStep`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-SpaceStep)<br>`1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `3` | Spacing as a step of the 4 px spacing steps (1–8).<br>Shown in: [A column](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-column), [A row](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-row), [Gaps](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#gaps) and 164 more |
| `align` | `CSSProperties["alignItems"]` | — | Alignment across the direction – `alignItems` unchanged.<br>Shown in: [A row](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-row), [Gaps](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#gaps), [A tour summary](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-tour-summary) and 78 more |
| `justify` | `CSSProperties["justifyContent"]` | — | Distribution along the direction – `justifyContent` unchanged.<br>Shown in: [A row](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-row), [A tour summary](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-tour-summary), [Name the same action in every row (VisuallyHidden)](https://romanhaendler.github.io/umriss-ui/core/visuallyhidden/#actions-in-rows) and 9 more |
| `wrap` | `boolean` | `false` | Lets the children wrap when the line does not suffice.<br>Shown in: [Gaps](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#gaps), [Wrapping](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#wrapping), [Sized to the value (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#sized-to-the-value) and 32 more |
| `ref` | `Ref<HTMLDivElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of `<div>`.

### `GridProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `columns` | `number \| readonly `[`GridColumnWidth`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-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.<br>Shown in: [Fixed columns](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#fixed-columns), [Columns of their own widths](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#columns-of-their-own-widths), [A token for one region (Theming)](https://romanhaendler.github.io/umriss-ui/core/theming/#a-token-for-one-region) and 5 more |
| `minItemWidth` | `string` | — | Responsive: as many columns as fit at this minimum width; wins over `columns`, a list included.<br>Shown in: [Columns by width](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#columns-by-width), [A tour summary](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#a-tour-summary), [A dark region (Theming)](https://romanhaendler.github.io/umriss-ui/core/theming/#a-dark-region) and 51 more |
| `gap` | [`SpaceStep`](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#type-SpaceStep)<br>`1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `4` | Spacing as a step of the 4 px spacing steps (1–8); applies in both directions.<br>Shown in: [Fixed columns](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#fixed-columns), [Columns by width](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#columns-by-width), [Columns of their own widths](https://romanhaendler.github.io/umriss-ui/core/stack-and-grid/#columns-of-their-own-widths) and 58 more |
| `ref` | `Ref<HTMLDivElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) 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`.

```ts
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.

```ts
type GridColumnWidth = number | "fill";
```

## Known limits

- No breakpoints and no responsive props: a grid follows the width through `minItemWidth`, anything else is your stylesheet's.
- Tiles are not arranged by the user: for a dashboard the reader arranges, see `@umriss-ui/dashboard` ([ADR-0064](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0064-a-dashboard-a-user-arranges-is-a-sequence-on-fixed-columns.md)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
