DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Stack, HStack, VStack } from "@tessinaui/ui";
{/* 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>

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.

UseWhen
StackOne axis. You want a gap, maybe a divider, maybe a different axis on phones.
FlexYou need the full flexbox surface — independent rowGap/columnGap, align-content, wrap-reverse — or per-child sizing via FlexItem.
GridTwo axes.
ContainerConstraining page width, not arranging children.
SpacerPushing one item to the far edge: <Spacer grow /> between two groups.
DividerA rule that stands on its own, rather than one repeated between children.

Two rules worth taking from the platforms rather than re-deriving:

  • Prefer whitespace to a rule. Material's guidance is that dividers are for when whitespace cannot do the job, and both it and the app screens surveyed for this component separate groups far more often than they rule every item. A gap="lg" between two groups usually reads better than a rule between six rows.
  • When a row is anchored by an avatar or icon, inset the rule so it starts at the row's primary text. Apple encodes this in API (separatorInset, listRowSeparatorLeading) and Material calls it the inset divider; here it is --stack-divider-inset. See Dividers.

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

HStack and VStack

Direction

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

It also accepts an object keyed by breakpoint:

<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:

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

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

<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:

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

<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:

<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:

<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:

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

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

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.
Split ButtonStatus

On this page

PlaygroundInstallationUsageWhen to useExamplesDefaultDirectionResponsiveGapAlignmentWrapDividersMedia objectReverseInlinePolymorphic renderRTLHStack and VStackDirectionGaprowGapAlign and justifyWrap and inlineDividersInsetOne-off rulesPolymorphic renderHStack and VStackRTLAccessibilityAssistive technology matrixAPI ReferenceStack PropsHStack / VStackExportsData attributesCSS custom propertiesNotes