# Stack

> Flex stack primitive for vertical and horizontal rhythm.

- Status: stable
- Group: layout
- Page: https://www.digitaltableteur.com/design-system/components/stack
- Contract: https://www.digitaltableteur.com/contracts/v1/Stack.contract.json

## When to use

Stack is the default way to space siblings: a flex column (or row) whose `gap` comes from a fixed scale (Tailwind gap-1 to gap-8 classes: 4, 8, 16, 24 and 32px, not named DS space tokens), so vertical rhythm stays on the grid without margin fiddling. Use `as="ul"` with `li` children when the stack is semantically a list. If items need individual offsets or non-token gaps, that's FlexBox; if both axes matter, Grid.

## When not to use

- Do not nest Stacks purely to sum gaps; pick the right gap token on one Stack instead.
- Do not add margins to Stack children; the gap owns the spacing and margins fight it.
- Do not use it when items need individual offsets or a non-token gap (that's FlexBox) or when both axes matter (that's Grid).

## Look-alikes

- Card: Do not wrap page sections or whole forms in cards, and never nest cards in cards; that is Section/Stack territory.

## Props

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `children` | `ReactNode` | none | Stacked children. | Yes |
| `direction` | `"horizontal" \| "vertical"` | `vertical` | Stack axis. Defaults to `vertical`. | No |
| `gap` | `"sm" \| "md" \| "lg" \| "xl" \| "xs" \| "none"` | `md` | Gap step between items, mapped to Tailwind gap classes (xs 4px, sm 8px, md 16px, lg 24px, xl 32px). Defaults to `md`. | No |
| `align` | `"center" \| "stretch" \| "end" \| "start"` | `stretch` | Cross-axis alignment. Defaults to `stretch`. | No |
| `justify` | `"center" \| "end" \| "start" \| "between" \| "around"` | `start` | Main-axis distribution. Defaults to `start`. | No |
| `wrap` | `boolean` | `false` | Allow wrapping on horizontal stacks. Defaults to `false`. | No |
| `className` | `string` | none | Stack class names. | No |
| `as` | `"symbol" \| "object" \| "image" \| "form" \| "slot" \| "style" \| "title" \| "button" \| "search" \| "article" \| "dialog" \| "figure" \| "img" \| "link" \| "main" \| "menu" \| "menuitem" \| "option" \| "switch" \| "table" \| "text" \| "time" \| "div" \| "section" \| "aside" \| "li" \| "a" \| "abbr" \| "address" \| "area" \| "audio" \| "b" \| "base" \| "bdi" \| "bdo" \| "big" \| "blockquote" \| "body" \| "br" \| "canvas" \| "caption" \| "center" \| "cite" \| "code" \| "col" \| "colgroup" \| "data" \| "datalist" \| "dd" \| "del" \| "details" \| "dfn" \| "dl" \| "dt" \| "em" \| "embed" \| "fieldset" \| "figcaption" \| "footer" \| "h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "head" \| "header" \| "hgroup" \| "hr" \| "html" \| "i" \| "iframe" \| "input" \| "ins" \| "kbd" \| "keygen" \| "label" \| "legend" \| "map" \| "mark" \| "meta" \| "meter" \| "nav" \| "noindex" \| "noscript" \| "ol" \| "optgroup" \| "output" \| "p" \| "param" \| "picture" \| "pre" \| "progress" \| "q" \| "rp" \| "rt" \| "ruby" \| "s" \| "samp" \| "script" \| "select" \| "small" \| "source" \| "span" \| "strong" \| "sub" \| "summary" \| "sup" \| "template" \| "tbody" \| "td" \| "textarea" \| "tfoot" \| "th" \| "thead" \| "tr" \| "track" \| "u" \| "ul" \| "var" \| "video" \| "wbr" \| "webview" \| "svg" \| "animate" \| "animateMotion" \| "animateTransform" \| "circle" \| "clipPath" \| "defs" \| "desc" \| "ellipse" \| "feBlend" \| "feColorMatrix" \| "feComponentTransfer" \| "feComposite" \| "feConvolveMatrix" \| "feDiffuseLighting" \| "feDisplacementMap" \| "feDistantLight" \| "feDropShadow" \| "feFlood" \| "feFuncA" \| "feFuncB" \| "feFuncG" \| "feFuncR" \| "feGaussianBlur" \| "feImage" \| "feMerge" \| "feMergeNode" \| "feMorphology" \| "feOffset" \| "fePointLight" \| "feSpecularLighting" \| "feSpotLight" \| "feTile" \| "feTurbulence" \| "filter" \| "foreignObject" \| "g" \| "line" \| "linearGradient" \| "marker" \| "mask" \| "metadata" \| "mpath" \| "path" \| "pattern" \| "polygon" \| "polyline" \| "radialGradient" \| "rect" \| "set" \| "stop" \| "textPath" \| "tspan" \| "use" \| "view"` | `div` | Polymorphic element. "ul" and "ol" get role="list" (unless you pass a role) so Safari/VoiceOver keep list semantics under list-style: none. Defaults to `div`. Typed as any React ElementType, so the values list is every JSX intrinsic; only container or list elements (div, section, nav, ul, ol) make sense here. | No |

## Variants

- Allowed `align` values: `stretch`, `start`, `center`, `end` (default `stretch`).
- Allowed `direction` values: `horizontal`, `vertical` (default `vertical`).
- Available gaps: `sm`, `md`, `lg`, `xl`, `xs`, `none` (default `md`).
- Allowed `justify` values: `center`, `end`, `start`, `between`, `around` (default `start`).
- Allowed `as` values: `symbol`, `object`, `image`, `form`, `slot`, `style`, `title`, `button`, `search`, `article`, `dialog`, `figure`, `img`, `link`, `main`, `menu`, `menuitem`, `option`, `switch`, `table`, `text`, `time`, `div`, `section`, `aside`, `li`, `a`, `abbr`, `address`, `area`, `audio`, `b`, `base`, `bdi`, `bdo`, `big`, `blockquote`, `body`, `br`, `canvas`, `caption`, `center`, `cite`, `code`, `col`, `colgroup`, `data`, `datalist`, `dd`, `del`, `details`, `dfn`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `footer`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `head`, `header`, `hgroup`, `hr`, `html`, `i`, `iframe`, `input`, `ins`, `kbd`, `keygen`, `label`, `legend`, `map`, `mark`, `meta`, `meter`, `nav`, `noindex`, `noscript`, `ol`, `optgroup`, `output`, `p`, `param`, `picture`, `pre`, `progress`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `script`, `select`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `template`, `tbody`, `td`, `textarea`, `tfoot`, `th`, `thead`, `tr`, `track`, `u`, `ul`, `var`, `video`, `wbr`, `webview`, `svg`, `animate`, `animateMotion`, `animateTransform`, `circle`, `clipPath`, `defs`, `desc`, `ellipse`, `feBlend`, `feColorMatrix`, `feComponentTransfer`, `feComposite`, `feConvolveMatrix`, `feDiffuseLighting`, `feDisplacementMap`, `feDistantLight`, `feDropShadow`, `feFlood`, `feFuncA`, `feFuncB`, `feFuncG`, `feFuncR`, `feGaussianBlur`, `feImage`, `feMerge`, `feMergeNode`, `feMorphology`, `feOffset`, `fePointLight`, `feSpecularLighting`, `feSpotLight`, `feTile`, `feTurbulence`, `filter`, `foreignObject`, `g`, `line`, `linearGradient`, `marker`, `mask`, `metadata`, `mpath`, `path`, `pattern`, `polygon`, `polyline`, `radialGradient`, `rect`, `set`, `stop`, `textPath`, `tspan`, `use`, `view` (default `div`).

## Invalid combinations

The contract records no invalid combinations.

## Keyboard

The contract records no keyboard interaction.

## Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

| Level | Rule | How it is checked |
| --- | --- | --- |
| MUST | ARIA: Stack is a presentational flex container with no role of its own; it inherits the semantics of its `as` element (default `div`). Use `as="ul"`/`"ol"` with `li` children when the group is semantically a list. Tailwind preflight sets list-style none, and Safari with VoiceOver then drops list semantics unless role="list" is set, so Stack adds role="list" to ul and ol unless a role is passed. Other HTML attributes (role, aria-*, id, data-*) are forwarded to the rendered element. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by `npm run test:stories:matrix:ci`. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by `npm run test:stories:matrix:ci`. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by `npm run test:stories:hc`. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Default to gap="md" (16px) for content rhythm; xs/sm for dense clusters, lg/xl between groups. | Not stated in the contract. |
| SHOULD | Use direction="horizontal" with align="center" for inline clusters like actions or metadata rows. | Not stated in the contract. |
| SHOULD | Set wrap for chip/tag rows so they reflow instead of overflowing. | Not stated in the contract. |
| SHOULD | Use as="ul"/"ol" (with li children) when the group is semantically a list; Safari with VoiceOver may still drop the list role, because Stack cannot pass role="list". | Not stated in the contract. |
| SHOULD NOT | Do not nest Stacks purely to sum gaps; pick the right gap on one Stack. | Not stated in the contract. |
| SHOULD NOT | Do not add margins to Stack children; the gap owns the spacing, margins fight it. | Not stated in the contract. |

## Install and import

- Install: `npm install @digitaltableteur/react` (version 0.9.1, restricted access on npm).
- Import: `import { Stack } from "@digitaltableteur/react";`
- Entry point: `@digitaltableteur/react/layout` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once at the app boundary.
- Also available as the custom element `<dt-stack>`; see the custom elements manifest.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Stack.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/stack.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.
