Stack
Vertical layout primitive. It owns the space between its children, so nothing inside needs a margin.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Stack is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Stack } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Vertical layout primitive. It owns the space between its children, so nothing inside needs a margin.
Use it when
- Anything stacks vertically: form fields, card contents, page sections.
- You would otherwise write
margin-bottomon every child but the last.
Don't use it when
- The layout is horizontal. Use
Inline. - Children need to wrap onto multiple rows. Use
Grid.
Example
<Stack gap="lg">
<Heading>Billing</Heading>
<Card>…</Card>
</Stack>
Layers and spacing
gap picks a step of the stack scale. layer says instead how closely the things either side of the gap belong together, and lets the layout's character decide the distance:
<Stack layer="section" spacing="open">
<Stack layer="group">…</Stack>
<Stack layer="group">…</Stack>
</Stack>
related: parts of one thing, such as a label and its value.group: members of a set, such as the fields in a form.block: one unit from the next, such as a card from a card.section: a theme from the next within a region.
For text, eyebrow sets the gap between an eyebrow and its heading, subcopy between a heading and the lead under it, and paragraph between one paragraph and the next. Nest them: an eyebrow stack of the eyebrow and heading, inside a subcopy stack with the lead.
spacing="tight" pulls the layers together for a technical screen; open leaves related things close and moves the layers apart. It sets data-layout on the stack, so everything inside follows. See Foundations, Layout.
Composition
Nests freely inside Inline, Grid, and itself. as lets it render as section, ul, or form without a wrapper.
Tokens
--dt-space-stack-* for gap, and --dt-layout-stack-* and --dt-layout-text-* for layer, multiplied by --dt-layout-scale (1 on a page). The context layer retunes xl and 2xl, so a marketing page gets more air than a dashboard from the same prop.
Content
None. Stack renders no text of its own.
Props
The typed contract, from Stack.d.ts.
import * as React from "react";
/** Vertical layout. Owns the gap between children so they never set their own margins. */
export interface StackProps extends React.HTMLAttributes<HTMLElement> {
/** Gap from the stack axis of the space scale. @default "md" */
gap?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
/** How closely the things either side of this gap belong together, from the layout layers: `related` (parts of one thing), `group` (members of a set), `block` (one unit from the next), `section` (a theme from the next); or, for text, `eyebrow` (an eyebrow and its heading), `subcopy` (a heading and the lead under it), `paragraph` (one paragraph and the next). Sets the gap from `--dt-layout-stack-*` or `--dt-layout-text-*`, which the layout's character moves as one, and wins over `gap`. */
layer?: "related" | "group" | "block" | "section" | "eyebrow" | "subcopy" | "paragraph";
/** The layout's character for this element and everything inside it: `tight` (technical), `balanced` or `open` (breathing room). Sets `data-layout`, which re-declares the layer tokens here. Inherited from the page when not set. */
spacing?: "tight" | "balanced" | "open";
align?: React.CSSProperties["alignItems"];
justify?: React.CSSProperties["justifyContent"];
/** Element to render. @default "div" */
as?: keyof React.JSX.IntrinsicElements;
children?: React.ReactNode;
}
export declare function Stack(props: StackProps): React.JSX.Element;
Tokens it reads
Every custom property Stack.jsx resolves, read from the source: 15 semantic. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
| Token | Tier | Declared as |
|---|---|---|
--dt-layout-scale | semantic | 1 |
--dt-layout-stack-block | semantic | var(--dt-dim-8) |
--dt-layout-stack-group | semantic | var(--dt-dim-4) |
--dt-layout-stack-related | semantic | var(--dt-dim-2) |
--dt-layout-stack-section | semantic | var(--dt-dim-16) |
--dt-layout-text-eyebrow | semantic | var(--dt-space-stack-sm) |
--dt-layout-text-paragraph | semantic | var(--dt-space-stack-md) |
--dt-layout-text-subcopy | semantic | var(--dt-space-stack-sm) |
--dt-space-stack-2xl | semantic | var(--dt-dim-16) |
--dt-space-stack-2xs | semantic | var(--dt-dim-1) |
--dt-space-stack-lg | semantic | var(--dt-dim-6) |
--dt-space-stack-md | semantic | var(--dt-dim-4) |
--dt-space-stack-sm | semantic | var(--dt-dim-3) |
--dt-space-stack-xl | semantic | var(--dt-dim-10) |
--dt-space-stack-xs | semantic | var(--dt-dim-2) |
Source
React only, no dependencies, every value a token.
import React from "react";
const GAPS = { "2xs": "var(--dt-space-stack-2xs)", xs: "var(--dt-space-stack-xs)", sm: "var(--dt-space-stack-sm)", md: "var(--dt-space-stack-md)", lg: "var(--dt-space-stack-lg)", xl: "var(--dt-space-stack-xl)", "2xl": "var(--dt-space-stack-2xl)" };
/* Each layer is drawn at --dt-layout-scale, 1 on a page, so a surface at
another size (a social artboard) keeps its proportions. */
const scaled = (token) => `calc(var(${token}) * var(--dt-layout-scale, 1))`;
const LAYERS = {
related: scaled("--dt-layout-stack-related"), group: scaled("--dt-layout-stack-group"), block: scaled("--dt-layout-stack-block"), section: scaled("--dt-layout-stack-section"),
eyebrow: scaled("--dt-layout-text-eyebrow"), subcopy: scaled("--dt-layout-text-subcopy"), paragraph: scaled("--dt-layout-text-paragraph"),
};
export function Stack({ gap = "md", layer, spacing, align, justify, as: Tag = "div", children, style, ...rest }) {
return (
<Tag data-layout={spacing} style={{ display: "flex", flexDirection: "column", gap: LAYERS[layer] || GAPS[gap] || GAPS.md, alignItems: align, justifyContent: justify, ...style }} {...rest}>
{children}
</Tag>
);
}
In context
Stack also appears on the primitives reference cards, alongside the rest of its family.