Stack
Vertical layout primitive — flex column with consistent gap, align, and justify props.
Stack
Vertical layout primitive — a
<div>configured asflex flex-colwith consistentgap,align, andjustifyprops.
Stack is the right choice any time you have items flowing
top-to-bottom with consistent spacing — card contents, form
sections, settings rows, vertical menus. For row layouts use
Flex (coming soon); for two-axis grids use Grid (coming soon);
for one-off styled divs without layout intent use Box (coming
soon).
Preview
Installation
Stack ships with @kalvner/kds. Import it via its dedicated subpath
for guaranteed tree-shaking (per ADR-012):
import { Stack } from "@kalvner/kds/containers/stack";The barrel root export (@kalvner/kds) exists as a fallback but
the path-per-component subpath is preferred for new code.
Anatomy
<Stack gap="md" align="stretch" justify="start">
├─ child 1
├─ child 2
└─ child 3
</Stack>Stack renders a single <div> with classes:
flex flex-col gap-{gap} items-{align} justify-{justify}It accepts every native div prop (id, className,
onClick, ARIA attributes, etc.) and forwards them through. The
ref prop forwards to the underlying div — no forwardRef wrapper
needed thanks to React 19's ref-as-prop API.
Usage
When to use
- Any list of items flowing top-to-bottom
- Form sections (label, input, helper text)
- Card body composition (heading, description, action)
- Vertical navigation menus
- Any time you'd reach for
flex flex-col gap-Nyou should use Stack instead
When NOT to use
- Row layouts — use
Flex(coming soon) - Grid / two-dimensional layouts — use
Grid(coming soon) - One-off styled divs without layout — use
Box(coming soon) - Very long lists — prefer a virtualised list (e.g. TanStack Virtual) since Stack renders every child eagerly
- Nesting just to add spacing — Stack handles spacing itself; a Stack-of-Stacks is the right pattern when you genuinely need hierarchical groups, not for spacing alone
Examples
Gap scale
All seven gap values, from none (no spacing) to 2xl (3rem). Pick
the smallest gap that reads as separation.
gap="none"
gap="xs"
gap="sm"
gap="md"
gap="lg"
gap="xl"
gap="2xl"
Cross-axis alignment
Cross-axis alignment maps to CSS align-items. stretch (default)
makes children fill the container width.
Main-axis distribution
Main-axis distribution maps to CSS justify-content. Only visible
when the Stack has a fixed height larger than its content.
Composition
Stack composes recursively — a Stack child can itself be a Stack with its own gap and alignment. This is the canonical pattern for form panels and card bodies:
Notifications
Choose which events generate an alert.
<Stack gap="lg">
<header>...</header>
<Stack gap="md">
<Stack gap="xs">
<span>Direct mentions</span>
<span>Anyone tags you in a comment.</span>
</Stack>
{/* ... */}
</Stack>
<Stack gap="sm">
<button>Save changes</button>
<button>Cancel</button>
</Stack>
</Stack>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
gap | 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'md' | Spacing between items via Tailwind's gap scale |
align | 'start' | 'center' | 'end' | 'stretch' | 'baseline' | 'stretch' | Cross-axis alignment (align-items) |
justify | 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | 'start' | Main-axis distribution (justify-content) |
In addition to the props above, Stack accepts every standard div
prop (forwarded to the rendered element). The ref prop is
forwarded to the underlying <div> via React 19's ref-as-prop API.
Accessibility
Stack is a layout primitive only — it does not introduce any
ARIA roles or interactive behaviour. It renders an unstyled,
semantically-neutral <div> and inherits the surrounding document
flow.
| Concern | Behaviour |
|---|---|
| Roles | None added. Pass role="..." if the content needs one (e.g. role="list" for menus). |
| Keyboard navigation | Stack itself is non-interactive and not focusable. Children handle their own keyboard semantics. |
| Screen readers | Stack is invisible to AT — only its children are announced. |
| Focus management | None. Use the tabIndex and onFocus props of children if you need focus-trap or skip-link behaviour. |
| Touch targets | Spacing between children comes from gap — pick md (1rem) or larger if children are touch targets, to keep them ≥ 44×44 CSS px when stacked. |
| Motion | None. |
If you're stacking semantically-related items, mark the wrapping
element accordingly — <Stack as="nav"> or role="list" makes the
relationship explicit for assistive tech.
Do / Don't
Do
- Pick the smallest
gapthat still reads as separation. Crowded layouts read as denser; spaced ones read as more important. - Compose Stack with itself — nested Stacks are the canonical way to build form panels and card bodies.
- Use
justify="between"when you genuinely have content + actions at the top and bottom of a fixed-height container. - Pass through ARIA roles (
role="list",aria-labelledby) on the Stack itself when the children form a semantic group.
Don't
- Don't use Stack as a generic styled wrapper — that's
Box(coming soon). - Don't rely on Stack for row layouts — use
Flex(coming soon) withdirection="row"instead. - Don't manually add
mb-Nto children — that's whatgapis for. Mixed margins + gap leads to inconsistent spacing. - Don't render hundreds of items — virtualise instead. Stack doesn't lazy-render.
Related
Flex(coming soon) — row layouts withdirection,wrap,align,justifypropsGrid(coming soon) — two-axis CSS Grid withcols/rows/gapBox(coming soon) — generic styled<div>without layout intentSpacer(coming soon) — fills remaining flex space (used inside Flex more than Stack)