apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Stack
Variant↳ other

Layout

Stack

Flex-based layout vocabulary.

Stack

<Stack />, <HStack />, <VStack />, and <Spacer /> are the DS's flex-based layout vocabulary. They cover ~95% of layout composition in product surfaces so consumers can stop reaching for raw <div className="flex …"> everywhere.

Overview — vertical stack with title, body, and actions

Loading preview…
Overview.tsx
  • <Stack> — generic flex container. Defaults to direction="column".
  • <HStack> — same surface, direction locked to row. Reads better at the call site.
  • <VStack> — same surface, direction locked to column. Reads as explicit intent.
  • <Spacer> — flex: 1 separator (or a fixed-size span). Partner primitive that the divider inserter recognizes via the __sds_spacer marker.

Anatomy

tsx
import { HStack, Spacer, Stack, VStack } from 'apx-ds';

<Stack gap={4}>
  <Card>One</Card>
  <Card>Two</Card>
</Stack>

<HStack gap={2} align="center">
  <Avatar />
  <span>Ada Lovelace</span>
  <Spacer />
  <Button>Edit</Button>
</HStack>
import { HStack, Spacer, Stack, VStack } from 'apx-ds';

<Stack gap={4}>
  <Card>One</Card>
  <Card>Two</Card>
</Stack>

<HStack gap={2} align="center">
  <Avatar />
  <span>Ada Lovelace</span>
  <Spacer />
  <Button>Edit</Button>
</HStack>

Stack has no visual variants — it's invisible by design. Only layout axes:

AxisValuesDefault
directioncolumn · column-reverse · row · row-reversecolumn
alignstart · center · end · stretch · baselinestretch
justifystart · center · end · between · around · evenlystart
gap0 · px · 0.5 · 1 · 2 · 3 · 4 · 5 · 6 · 8 · 10 · 12undefined
wrapfalse · true · reversefalse
inlinefalse · truefalse
fullWidthfalse · truefalse

Every axis accepts a ResponsiveValue<T> — pass { base: 'column', md: 'row' } and the recipe emits flex-col md:flex-row via the engine's native responsive support.

Gap (and overrides)

gap controls both axes; rowGap (Tailwind gap-y-*) and columnGap (Tailwind gap-x-*) split the spacing per axis when set. When only one override is provided the other axis falls back to the unified gap value:

tsx
<Stack gap={2} />                     {/* gap-2 on both axes */}
<Stack gap={2} rowGap={4} />          {/* gap-x-2 (cols) + gap-y-4 (rows) */}
<Stack rowGap={1} columnGap={2} />    {/* gap-y-1 + gap-x-2, no unified gap */}
<Stack gap={2} />                     {/* gap-2 on both axes */}
<Stack gap={2} rowGap={4} />          {/* gap-x-2 (cols) + gap-y-4 (rows) */}
<Stack rowGap={1} columnGap={2} />    {/* gap-y-1 + gap-x-2, no unified gap */}

The 13-entry spacing scale maps 1:1 onto Tailwind's standard gap-* ladder; 'px' covers the 1px hairline gap.

Dividers

Pass any node to divider and Stack auto-inserts it between adjacent non-Spacer siblings:

tsx
<Stack divider={<hr className="h-px bg-border border-0" />} gap={2}>
  <Item />
  <Item />
  <Item />
</Stack>
<Stack divider={<hr className="h-px bg-border border-0" />} gap={2}>
  <Item />
  <Item />
  <Item />
</Stack>
  • Spacer edges are skipped — a Spacer already creates separation.
  • Falsy children (false, null, undefined) are filtered before counting siblings, so conditional rendering doesn't produce orphan dividers.
  • Element dividers are cloned with a stable key per insertion; primitives are wrapped in a keyed <Fragment>.

Spacer

Two modes:

tsx
<HStack>
  <Cancel />
  <Spacer />              {/* greedy: flex:1, pushes the next sibling to the end */}
  <Save />
</HStack>

<VStack>
  <Header />
  <Spacer size={6} />     {/* fixed 24px span on the parent's main axis */}
  <Body />
</VStack>
<HStack>
  <Cancel />
  <Spacer />              {/* greedy: flex:1, pushes the next sibling to the end */}
  <Save />
</HStack>

<VStack>
  <Header />
  <Spacer size={6} />     {/* fixed 24px span on the parent's main axis */}
  <Body />
</VStack>

Spacer is aria-hidden="true". Use axis="block" to pin a fixed Spacer to the vertical axis when the parent isn't a Stack or you want to override the auto-detection.

Polymorphism

as swaps the root element (<nav>, <ul>, <section>, …). For the rare case where the element already exists (e.g. a router <Link>), use asChild to merge Stack's props onto the single child via <Slot>:

tsx
<Stack asChild gap={2}>
  <a href="/x">…</a>             {/* renders the <a>, with flex + gap-2 applied */}
</Stack>
<Stack asChild gap={2}>
  <a href="/x">…</a>             {/* renders the <a>, with flex + gap-2 applied */}
</Stack>

as and asChild are mutually exclusive — a dev warning fires when both are set; asChild wins at runtime.

Examples

Basic — vertical stack with gap

Loading preview…
Basic.tsx

Horizontal — HStack with align center

Loading preview…
Horizontal.tsx

Responsive — direction column → row at md

Loading preview…
Responsive.tsx

With dividers — auto-inserted between siblings

Loading preview…
WithDividers.tsx

With Spacer — push actions to opposite ends

Loading preview…
WithSpacer.tsx

Justify — all six distribution values

Loading preview…
Justify.tsx

Align — all five cross-axis values

Loading preview…
Align.tsx

Gap — spacing scale from 0 to 8

Loading preview…
Gap.tsx

Wrap — flex-wrap with badges

Loading preview…
Wrap.tsx

Polymorphic — as='nav' and as='ul'

Loading preview…
PolymorphicAs.tsx

asChild — wrap an anchor as the Stack root

Loading preview…
AsChild.tsx

Nested — HStack inside VStack for a card row

Loading preview…
Nested.tsx

Accessibility

  • Stack itself carries no implicit role — it's a layout primitive (<div> by default).
  • For lists, use as="ul" + <li> children, or role="list" on the Stack with role="listitem" on children. axe-core: zero violations across the matrix.
  • Spacer is aria-hidden="true" (purely visual).
  • No focus management — layout doesn't carry focus semantics.

RTL

flex-direction: row flows logical-start → logical-end natively in both LTR and RTL. justify-start / justify-end are also logical. align-* operates on the cross axis and is direction-agnostic. No RTL-specific code anywhere in Stack.

Theming

Stack and the two alias components (HStack / VStack) all read from theme.components.Stack.styleOverrides.root — override once, applies to all three. Spacer reads from theme.components.Spacer.styleOverrides.root. Consumer className always wins via tailwind-merge.

Do / Don't

  • Do reach for <Stack> / <HStack> / <VStack> instead of <div className="flex …">.
  • Do put <Spacer /> between siblings to push them apart in an HStack toolbar.
  • Do use as="nav" / as="ul" to add semantic intent without losing the layout primitive.
  • Don't add margin to children — Stack handles spacing via gap. Margins compound with gap and produce inconsistent rhythm.
  • Don't mix as and asChild — pick one.
  • Don't reach for direction="row-reverse" in LTR-only layouts. row already flips correctly in RTL; row-reverse is for the rare "always reverse" intent.

Props

PropTypeDefaultDescription
alignResponsiveValue<StackAlign>'stretch'Cross-axis alignment.
asStackAs—Render element. Defaults to `'div'`. Use semantic elements (`'nav'`, `'ul'`, …) when relevant.
asChildboolean—Radix-style polymorphism. Merges Stack's props onto a single child element (e.g. a router Link). Cannot combine with `as` — a runtime guard fires in dev when both are set.
columnGapResponsiveValue<StackGap>—Override gap on the column axis only. Equivalent to CSS `column-gap` / Tailwind `gap-x-*`. When set, Stack emits `gap-x-{columnGap}` instead of letting `gap` cover both axes.
directionResponsiveValue<StackDirection>'column'Main axis direction.
dividerReactNode—Auto-insert this node between non-Spacer siblings. Killer feature for "card" / "list" rows: pass `<Divider />` once and Stack injects it n−1 times. Spacer edges are skipped (Spacers already create separation).
fullWidthboolean—Add `w-full` — useful for the 99% Stack-fills-its-container case in product UIs.
gapResponsiveValue<StackGap>undefined (no gap)Gap between children (both axes). Numeric values map 1:1 onto the theme spacing scale; `'px'` is a 1px hairline. Accepts `ResponsiveValue` so you can change density per breakpoint.
inlinebooleanfalseRender `inline-flex` instead of `flex`. Useful for inline button rows.
justifyResponsiveValue<StackJustify>'start'Main-axis distribution.
refRef<HTMLElement>—Ref to the rendered element. Concrete element type depends on `as` / `asChild`. Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
rowGapResponsiveValue<StackGap>—Override gap on the row axis only. Equivalent to CSS `row-gap` / Tailwind `gap-y-*`. When set, Stack emits `gap-y-{rowGap}` instead of letting `gap` cover both axes.
styleCSSProperties—Inline style override. Wins via CSS specificity over recipe + theme classes.
sxSx—Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars).
wrapStackWrapfalseWrap behavior.