--primitiv-stack-gapA Flexbox stack — direction picks the axis, gap the spacing (a density-scaled Context token per step), wrap whether the row may break onto additional lines, and align/justify the alignment, all as modifier classes. No inline styles: the alignment props are curated presets, not raw CSS keywords.
primitiv add stack installs it whichever mode you are reading.Playground
Preview
lines
import { Stack } from "@/components/ui/stack";
<Stack direction="column" gap="md" align="stretch" justify="start"> <div>One</div> <div>Two</div> <div>Three</div></Stack>Density is set by a data-density ancestor — the Context system, not a Stack prop.
Installation
npx primitiv add stackpnpm dlx primitiv add stackyarn dlx primitiv add stackbunx primitiv add stackImport
import { Stack } from "@/components/ui/stack";No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.
Props
Stack
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | — | Render the single child element instead of a wrapping <div>, merging
the stack classes onto it — e.g. <Stack asChild><ul>...</ul></Stack>. |
| direction | "column" | "row" | "column-reverse" | "row-reverse" | column | Flex axis, and its resting or reversed order — the four flex-direction keywords directly. |
| gap | "none" | "xs" | "sm" | "md" | "lg" | "xl" | md | Space between children; each step (except none) is a density-scaled Context token. |
| align | "start" | "center" | "end" | "stretch" | "baseline" | stretch | Cross-axis alignment (align-items), as a preset set rather than a raw CSS passthrough. |
| wrap | "nowrap" | "wrap" | nowrap | Whether children may wrap onto additional lines when the main axis runs out of room (flex-wrap). |
| justify | "start" | "center" | "end" | "between" | "around" | "evenly" | start | Main-axis distribution (justify-content), as a preset set rather than a raw CSS passthrough. |
Styling contract
Accessibility
- Stack renders a plain
<div>with no role or semantics — it is pure layout, so a screen reader sees straight through it to the children. - When the group is something — a list, a set of navigation links — use
asChildto render the right element (<ul>,<nav>,<ol>) instead of an anonymous<div>, so the structure is announced rather than flattened. row-reverseandcolumn-reversechange the visual order only. The DOM order is unchanged, so keyboard focus and screen-reader reading order still follow the source — reverse a stack and they will disagree with what is on screen. Reorder the markup instead when the reading order is meant to change too.gapis space between items, not padding — Stack adds no inset of its own, so it never introduces a focus-ring-clipping edge. The children keep their own hit areas.
Examples
Direction
direction sets the main axis. The default is column — a vertical stack — and row lays the children out horizontally. The two -reverse values flip the visual order; see the note under Accessibility, because reversing is visual only and does not move the DOM order focus and screen readers follow.
import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm"> <div>One</div> <div>Two</div> <div>Three</div></Stack>Gap
gap is a step on the space scale (none–xl), not a raw length — so the spacing stays on-system, and it rescales with the nearest data-density ancestor. Change the density above and the whole stack tightens or loosens: that is the Context system, resolved through --primitiv-stack-gap-*, not a fixed pixel value.
import { Stack } from "@/components/ui/stack";
<div data-density="comfortable"> <Stack gap="lg"> <div>One</div> <div>Two</div> <div>Three</div> </Stack></div>Alignment
Two axes: align positions children across the cross axis (the default stretch makes them fill it — visible here because the cells have different heights), and justify distributes them along the main axis, where between/around/evenly spread the free space. Both follow direction, so on a row they mean the opposite screen axes they would on a column.
import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm" align="center" justify="between"> <div>Short</div> <div>A taller cell</div> <div>End</div></Stack>Wrapping
wrap defaults to nowrap, so a row that runs out of space overflows. Set wrap="wrap" and the children break onto new lines instead — the usual choice for a row of tags or filters that must survive a narrow container. The gap applies between wrapped lines too.
import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm" wrap="wrap"> {tags.map((t) => ( <div key={t}>{t}</div> ))}</Stack>