Skip to content
Primitiv home
Framework
Consumption mode

Stack

stableSource Figma

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

Playground

Density

Preview

One
Two
lines
Three
Direction
Gap
Align
Justify
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 stack

Import

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.

PropTypeDefaultDescription
asChildbooleanRender 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"columnFlex axis, and its resting or reversed order — the four flex-direction keywords directly.
gap"none" | "xs" | "sm" | "md" | "lg" | "xl"mdSpace between children; each step (except none) is a density-scaled Context token.
align"start" | "center" | "end" | "stretch" | "baseline"stretchCross-axis alignment (align-items), as a preset set rather than a raw CSS passthrough.
wrap"nowrap" | "wrap"nowrapWhether children may wrap onto additional lines when the main axis runs out of room (flex-wrap).
justify"start" | "center" | "end" | "between" | "around" | "evenly"startMain-axis distribution (justify-content), as a preset set rather than a raw CSS passthrough.

Styling contract

--primitiv-stack-gap

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 asChild to render the right element (<ul>, <nav>, <ol>) instead of an anonymous <div>, so the structure is announced rather than flattened.
  • row-reverse and column-reverse change 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.
  • gap is 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.

Density
One
Two
Three
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 (nonexl), 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.

Density
One
Two
Three
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.

Density
Short
A taller cell
End
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.

Density
Design
Engineering
Product
Research
Marketing
Support
Operations
Finance
import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm" wrap="wrap">  {tags.map((t) => (    <div key={t}>{t}</div>  ))}</Stack>