DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
SurfaceContainerStackFlexGridSpacerDividerAspectRatioScroll Area
AI agentsContributing
  1. Components
  2. Stack

Stack

The spacing-first one-dimensional arranger. An axis, a gap, and an optional rule between children — with direction and gap that can change by breakpoint, so a column on the phone becomes a row on the desktop. Ships with HStack and VStack convenience wrappers.

$ pnpm add @tessinaui/ui

When to use

Stack arranges children along one axis and controls the space between them. That is the whole job: an axis, a gap, and optionally a rule.

  • One axis with a gap. A column of form fields, a row of actions, a cluster of chips: set gap and drop the margins on the children.
  • A different axis on phones. direction and gap take a breakpoint object, so a column on the phone becomes a row on the desktop.
  • Prefer whitespace to a rule. Use divider only when whitespace cannot do the job. A gap="lg" between two groups usually reads better than a rule between six rows.
  • A rule between repeated rows. divider draws one between each child and works out its own orientation. When a row is anchored by an avatar or icon, inset the rule so it starts at the row's primary text with --stack-divider-inset (see Dividers).
  • A group that means something. Use render to make the host a <nav> or <ul>, rather than leaving a <div> and adding ARIA to it.

When not to use

  • Two axes. Use Grid.
  • The fuller flexbox surface. A separate columnGap, wrap-reverse or per-child sizing: use Flex with FlexItem.
  • Constraining page width. That is Container, not arranging children.
  • Pushing one item to the far edge. Put Spacer with grow between two groups.
  • A rule that stands on its own. Use Divider rather than one repeated between children.
  • Interactive behaviour. Stack adds no role or keyboard behaviour; use the component built for the interaction, not a styled stack.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Stack, HStack, VStack } from "@tessinaui/ui";
example.tsx
{/* A column, the default */}
<Stack gap="md">
  <div>First</div>
  <div>Second</div>
</Stack>

{/* A column on the phone, a row from md up */}
<Stack direction={{ base: "vertical", md: "horizontal" }} gap={{ base: "sm", md: "md" }}>
  <Button variant="ghost">Cancel</Button>
  <Button variant="primary">Save</Button>
</Stack>

{/* A rule between each child, oriented automatically */}
<Stack gap="sm" divider>
  <span>Profile</span>
  <span>Security</span>
</Stack>

Examples

Default

A column with a medium gap.

Direction

The axis, fixed.

Responsive

direction and gap each take a breakpoint object. Note which way round it goes: on phones a two-action bar is almost always a vertical stack of full-width buttons, and the row is the desktop case.

Gap

The seven ladder steps.

Alignment

align on the cross axis, justify on the main one.

Wrap

A wrapping cluster, with rowGap opening the between-rows rhythm wider than the space between neighbouring chips.

Dividers

divider works out its own orientation. Pass a node when you want tones, dashes or a label — and note that a one-off rule needs no prop at all.

Media object

The most common horizontal composition there is, and where min-w-0, --stack-divider-inset and a two-tier gap rhythm all earn their keep.

Reverse

Visual order only — the DOM order does not move.

Inline

A stack that sits in the text flow.

Polymorphic render

A link row should be a <nav>; a list of items should be a <ul>.

RTL

On color

Inside a coloured Surface, the divider hairline draws in the plate's own ink instead of the page's grey. Stack reads the tone from the Surface, so there is nothing to pass. A custom divider node resolves its own tone.

HStack and VStack

Direction

direction is "horizontal" or "vertical", defaulting to "vertical".

It also accepts an object keyed by breakpoint:

example.tsx
<Stack direction={{ base: "vertical", md: "horizontal" }} />

The keys are base, sm, md, lg and xl — the same names as Tailwind's own variant prefixes, so there is no separate breakpoint table to learn.

Three behaviours are worth knowing:

  • Sparse objects forward-fill. { base: "vertical", lg: "horizontal" } is a column at every width below lg and a row from lg up. You only declare the bands where something changes.
  • A band below the first declared entry uses the prop's default. With { md: "horizontal" }, everything under md is a column — not a row, which is what an unset flex-direction would otherwise mean.
  • Values that agree everywhere collapse. { base: "horizontal" } emits the single class flex-row, not five banded ones.

Under the hood a responsive value resolves into five fixed, mutually disjoint bands (max-sm: · sm:max-md: · md:max-lg: · lg:max-xl: · xl:) rather than a bare utility plus a run of min-width variants. No viewport matches two bands, so stylesheet emission order cannot decide which one wins — the failure that once had a container measuring 24px where xl:px-12 asked for 48.

reverse flips the visual order. It is a single boolean across every band, and it changes paint order only: the DOM, the tab order and the screen-reader order are unchanged. Use it where those two orders are genuinely allowed to differ, and not otherwise.

Gap

gap is the space between children: none, xs, sm, md, lg, xl, 2xl — 0, 4, 8, 16, 24, 32 and 48px, the same ladder Flex and Grid use. The default is md.

It takes the same breakpoint object as direction, with the same forward-fill and collapse behaviour:

example.tsx
<Stack gap={{ base: "sm", lg: "xl" }} />

The resolved value is published as --stack-gap, so a one-off adjustment does not need a wrapper:

example.tsx
<Stack style={{ "--stack-gap": "13px" }} />

Gaps come in tiers, not one flat value. Across the settings and form screens surveyed, the space between a label and its field ran at roughly a third of the space between one field group and the next — commonly xs/sm inside a group against md/lg between them, and some blocks use a third, tighter tier again. Nesting is how you express that: an outer Stack at the group rhythm, inner ones at the tighter one. Do not reach for a single gap and split the difference.

rowGap

When a horizontal stack wraps, rowGap sets the space between rows, independently of the space between neighbouring items:

example.tsx
<Stack direction="horizontal" wrap gap="sm" rowGap="md" />

It requires wrap. Without it the prop is ignored rather than applied, because on an unwrapped column the cross-axis gap is the main gap and setting it here would silently fight gap.

Align and justify

align runs on the cross axis: start, center, end, stretch (the default), baseline. justify runs on the main axis: start (the default), center, end, between, around, evenly.

baseline is worth singling out. A section header — a title on the left, a "See all" action on the right — reads correctly only when the two sides sit on a shared baseline rather than being box-centred, and that pattern was near- universal in the screens surveyed.

Both props are scalar. If you need alignment to change by breakpoint, use className with Tailwind's own variants.

Wrap and inline

wrap turns on flex-wrap; pair it with rowGap above. A wrapped cluster's last row packs to the start — it is never justified.

inline swaps display: flex for inline-flex, so the stack sits in the text flow instead of taking a line of its own.

Dividers

divider puts a rule between each child — never before the first or after the last.

example.tsx
<Stack divider>…</Stack>                          {/* the house hairline */}
<Stack divider={<Divider tone="subtle" />}>…</Stack>  {/* anything richer */}

divider derives its own orientation from the axis: a row gets a vertical rule, a column a horizontal one, and a responsive stack gets the right one in each band. There is no orientation prop to pass, and therefore none to forget — a mistake other libraries leave available and catch nothing on.

The auto rule carries two classes that matter more than they look:

  • shrink-0 — a 1px flex child in a scrolling column is crushed to exactly height: 0 once content overflows. It renders perfectly in every short demo and vanishes in the real list, which is how the bug survives review.
  • self-stretch — without it, align="center" collapses the rule to zero length.

A custom node must carry both itself. <Divider /> already does.

Inset

Set --stack-divider-inset on a column to pull the rule's start edge in, so it begins at the row's primary text rather than at the frame:

example.tsx
<Stack divider style={{ "--stack-divider-inset": "3rem" }}>

Set it to the leading slot's width plus the row gap. It uses a logical margin, so it stays on the correct side under dir="rtl". The inset applies to a scalar-vertical stack; in a responsive stack the same margin would push the rule sideways in the row bands, so pass <Divider inset="start" /> as the node there instead.

One-off rules

The divider prop is for the repeating case. A single rule — the line above a checkout total, say — is just an ordinary child:

example.tsx
<Stack gap="sm">
  <Row label="Subtotal" />
  <Row label="Delivery" />
  <Divider tone="strong" />
  <Row label="Total" strong />
</Stack>

Polymorphic render

render swaps the host element while keeping the layout:

example.tsx
<Stack render={<nav aria-label="Account" />} direction="horizontal" gap="md" />
<Stack render={<ul />} gap="sm" divider>…</Stack>

Props merge rather than replace, and event handlers compose: an onClick on Stack and one on the host element both fire.

The <ul> case works because the interleaved rule is not wrapped in a container element — it is a direct, explicitly presentational child, so the list still contains only list items and presentational nodes. A wrapper <div> there would fail an accessibility audit with list (serious), and a unit test asserting the tag name would pass straight through it, because a tag name is not a role.

HStack and VStack

HStack and VStack are Stack with the axis locked. They take every other Stack prop, and they reject direction — including the silent case where a forwarding wrapper spreads direction: undefined onto them.

They are scalar by definition. A stack whose axis changes by breakpoint is Stack with a direction object.

RTL

A horizontal stack lays out along the inline axis, so it mirrors under dir="rtl" with no extra work: the first child sits on the right, and the gap is logical. The auto divider's inset uses ms-* rather than a physical margin, so an inset rule stays on the correct side in both directions.

dir is stamped on the element only when you pass it; otherwise direction is inherited.

API Reference

Stack Props

PropTypeDefaultDescription
direction"horizontal" | "vertical" | { base?, sm?, md?, lg?, xl? }"vertical"Layout axis; the object form varies it by breakpoint
reversebooleanfalseReverse the visual order. Paint order only
gapStackGap | { base?, sm?, md?, lg?, xl? }"md"Space between children
rowGapStackGap—Space between wrapped rows. Requires wrap
align"start" | "center" | "end" | "stretch" | "baseline""stretch"Cross-axis alignment
justify"start" | "center" | "end" | "between" | "around" | "evenly""start"Main-axis distribution
wrapbooleanfalseAllow children to wrap
inlinebooleanfalseRender as inline-flex
dividerboolean | ReactNode—true for a self-orienting house hairline, or any node
renderuseRender.RenderProp<div />Render as a different host element
dir"ltr" | "rtl"—Text direction. Stamped only when set
classNamestring—Extra classes on the root element

StackGap is "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl".

The component extends all standard div HTML attributes.

HStack / VStack

Identical to Stack with direction locked to "horizontal" and "vertical" respectively, and removed from the prop type.

Exports

ExportTypeDescription
StackcomponentThe arranger
HStack / VStackcomponentAxis-locked shorthands
stackVariantscvaThe non-responsive variants, for composing a stack elsewhere
StackPropstype
StackDirection, StackGap, StackAlign, StackJustify, StackDirtypeValue unions
StackBreakpointtype"base" | "sm" | "md" | "lg" | "xl"
ResponsiveStackDirection, ResponsiveStackGaptypeThe scalar-or-object prop types

Data attributes

Stable hooks for styling, testing and automation.

AttributeOnValue
data-slot="stack"root—
data-orientationroothorizontal | vertical — the base band's value on a responsive stack
data-slot="stack-divider"each auto rule—

CSS custom properties

PropertyDefaultEffect
--stack-gapthe gap stepSpace between children, when gap is scalar
--stack-gap-base … --stack-gap-xlthe per-band stepSet instead of --stack-gap when gap is responsive
--stack-row-gapthe rowGap stepSpace between wrapped rows
--stack-divider-inset0pxHow far the auto rule's start edge is pulled in, on a column

Notes

  • A lone Fragment is seen through: <Stack divider>{rows}</Stack> where rows is <>{a}{b}</> still draws a rule between a and b. A Fragment with siblings stays one deliberate group, and the rule goes around it.
  • Falsy children are dropped before rules are counted, so a conditionally rendered child cannot leave a rule with nothing on one side of it.
  • With justify="between" and a divider, the rules are flex items too and receive their share of the distributed space.

Accessibility

Stack is a layout primitive. It adds no role, no label and no keyboard behaviour, and it does not interfere with the semantics of its children — which is why render exists: reach for <nav>, <ul> or <section> when the group means something, rather than leaving a <div> and adding ARIA to it.

  • Interleaved dividers are decorative. They are hidden from assistive technology and marked presentational, so a screen reader announces the content and not the furniture. For a rule that carries meaning, pass <Divider decorative={false} />.
  • reverse does not reorder the DOM. Anyone using a keyboard or a screen reader follows the source order, not the painted one. Only use it where the two are allowed to differ.
  • Contrast is a property of what you put inside a Stack; the primitive paints nothing but the optional 1px rule, which uses the house --border token.
  • Touch-target sizing applies to the interactive children, not to the container.

Assistive technology matrix

ATStatus
VoiceOver (macOS)Passed (2026-09-10, Safari) — walking stack-render, the <nav> host announces as "Account navigation" and its links read individually; the <ul> host reads "Two-factor authentication / Recovery codes / Active sessions" consecutively. No divider is announced anywhere — the interleaved rules are silent, which is the behaviour the decorative markup is there to produce. Transcript in the variant ledger
NVDAUntested — not runnable on the maintainers' machines
JAWSUntested — not runnable on the maintainers' machines
ContainerFlex

On this page

When to useWhen not to useInstallationUsageExamplesDefaultDirectionResponsiveGapAlignmentWrapDividersMedia objectReverseInlinePolymorphic renderRTLOn colorHStack and VStackDirectionGaprowGapAlign and justifyWrap and inlineDividersInsetOne-off rulesPolymorphic renderHStack and VStackRTLAPI ReferenceStack PropsHStack / VStackExportsData attributesCSS custom propertiesNotesAccessibilityAssistive technology matrix