Browse docs
Browse docs
A 1D flex container with typed direction, alignment, and gap. The only component in @dashforge/tw that does flex — Box doesn't, Grid doesn't. When you read <Stack> in a JSX tree you instantly know it's flex; no <Box display="flex"> trap to hunt for in 500 lines of utility classes.
import { Stack, Button } from '@dashforge/tw';
<Stack direction="row" gap={3} justify="end">
<Button variant="text">Cancel</Button>
<Button color="primary">Save</Button>
</Stack>{/* Vertical stack — the default */}
<Stack gap={4}>
<TextField name="email" label="Email" />
<TextField name="password" type="password" label="Password" />
<Button type="submit" color="primary">Sign in</Button>
</Stack>
{/* Horizontal toolbar */}
<Stack direction="row" gap={2} align="center" justify="between">
<Typography variant="h3">Workspace</Typography>
<Button variant="outlined">Settings</Button>
</Stack>direction defaults to 'col' — the most common case for forms, sidebars, panels.
Row 1
Row 2
Row 3
import { Stack, Box, Typography } from '@dashforge/tw';
<Stack gap={3} sx="w-64">
<Box variant="outlined" p={3} rounded="md">
<Typography variant="body2">Row 1</Typography>
</Box>
<Box variant="outlined" p={3} rounded="md">
<Typography variant="body2">Row 2</Typography>
</Box>
<Box variant="outlined" p={3} rounded="md">
<Typography variant="body2">Row 3</Typography>
</Box>
</Stack><Stack direction="col">vertical (default)</Stack>
<Stack direction="row">horizontal</Stack>
<Stack direction="col-reverse">last item first, vertically</Stack>
<Stack direction="row-reverse">last item first, horizontally</Stack>align controls the cross-axis (items-*), justify controls the main-axis (justify-*):
Dashforge
import { Stack, Box, Typography } from '@dashforge/tw';
<Stack direction="row" gap={3} align="center" justify="between" sx="w-full">
<Stack direction="row" gap={2} align="center">
<Box variant="solid" color="primary" p={2} rounded="md">
<Typography variant="caption">DF</Typography>
</Box>
<Typography variant="body2">Dashforge</Typography>
</Stack>
<Typography variant="caption" color="muted">v0.2.0-beta</Typography>
</Stack>{/* Header pattern: title left, actions right */}
<Stack direction="row" align="center" justify="between">
<Typography variant="h3">Page title</Typography>
<Stack direction="row" gap={2}>
<Button variant="outlined">Settings</Button>
<Button color="primary">Save</Button>
</Stack>
</Stack>
{/* Centered hero content */}
<Stack align="center" justify="center" gap={4} fullHeight>
<Typography variant="h1">Welcome</Typography>
<Typography variant="body1" color="muted">Pick up where you left off</Typography>
<Button color="primary">Continue</Button>
</Stack>Token-scale steps (same set as Box spacing):
0 · '0.5' · 1 · 2 · 3 · 4 · 6 · 8 · 12 · 16 · 24
<Stack gap={1}>tight</Stack>
<Stack gap={4}>comfortable</Stack>
<Stack gap={8}>spacious</Stack><Stack direction="row" wrap gap={2}>
{tags.map(tag => <Chip key={tag}>{tag}</Chip>)}
</Stack>Without wrap, a row of children that overflows horizontally clips. With wrap, children flow to the next line.
The classic settings-list pattern — a thin separator between rows:
<Stack
gap={3}
divider={<Box variant="outlined" color="neutral" sx="h-px border-0 border-t" />}
>
<SettingRow label="Public access" />
<SettingRow label="Email notifications" />
<SettingRow label="2FA" />
</Stack>The divider node is rendered N-1 times between children. One child = no divider. Strings/numbers work too:
<Stack direction="row" divider=" · ">
<a href="/home">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</Stack>
{/* Renders: Home · About · Contact */}⚠️ Fragments count as one child. If you wrap items in <>...</>, the divider sees the fragment as a single boundary. Hoist items out of the fragment when you need a divider between them.
The canonical card pattern — Box for the surface, Stack for the rhythm:
<Box variant="outlined" rounded="xl" p={6}>
<Stack gap={4}>
<Typography variant="h3">Notification preferences</Typography>
<Stack gap={3}>
<SettingRow label="Email digest" />
<SettingRow label="Push notifications" />
</Stack>
<Stack direction="row" gap={2} justify="end">
<Button variant="text">Cancel</Button>
<Button color="primary">Save</Button>
</Stack>
</Stack>
</Box>as and asChild{/* `as` — semantic nav with flex layout */}
<Stack as="nav" direction="row" gap={4}>
<a href="/docs">Docs</a>
<a href="/about">About</a>
</Stack>
{/* `asChild` — Stack styles paint onto an existing element */}
<Stack direction="row" gap={2} asChild>
<ul>
<li>One</li>
<li>Two</li>
</ul>
</Stack>asChild wins over as when both are passed. Note: divider is silently ignored under asChild (Slot wraps a single child).
| You need | Use | Why |
|---|---|---|
| Vertical list of form fields | <Stack gap={3}> | 1D, uniform gap |
Header with [logo] [spacer] [actions] | <Stack direction="row" justify="between"> | 1D with single spacer between groups |
| Toolbar (buttons in a row) | <Stack direction="row" gap={2}> | 1D, fixed gap |
| Settings list with separators | <Stack gap={3} divider={...}> | 1D with built-in divider |
| 3 cards side-by-side, responsive | <Grid container><Grid xs={12} md={4}> | 2D, columns vary per breakpoint |
| Dashboard with mixed widths | <Grid container cols={12}> | 2D, per-item span |
| Asymmetric layout (sidebar 300px + content fill) | <Stack direction="row"> + child widths | 1D, no need for column math |
The rule: 1 axis → Stack, 2 axes → Grid. Asymmetric flex (one fixed + one fill) stays in Stack because you don't need column math.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'row' | 'col' | 'row-reverse' | 'col-reverse' | 'col' | Flex direction. |
align | 'start' | 'center' | 'end' | 'stretch' | 'baseline' | — | Cross-axis alignment (items-*). |
justify | 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | — | Main-axis alignment (justify-*). |
gap | SpacingStep | — | Gap between children (mirror of Box spacing scale). |
wrap | boolean | false | Allow children to wrap to the next line. |
fullWidth | boolean | false | w-full. |
fullHeight | boolean | false | h-full. |
divider | ReactNode | — | Node rendered N-1 times between children. |
as | ElementType | 'div' | Override the HTML tag. |
asChild | boolean | false | Render via Radix Slot onto the single child. Wins over as. Divider is ignored. |
sx | string | — | Utility-class override, merged via tailwind-merge. |
| ...rest | HTMLAttributes<HTMLDivElement> | — | Native attributes pass through. |
import { stackVariants } from '@dashforge/tw';
// stackVariants({ direction: 'row', gap: 4, justify: 'between' }) → className<Stack> you know it's flex without reading further.divider is runtime-only — TV can't encode the "render between children" logic as a class. The walk is O(n) where n is the number of children. For typical UI (n ≤ ~10) the cost is negligible.React.Children.toArray does not recursively flatten Fragments. If you need a divider between items inside a Fragment, hoist them out.<Grid>. Stack is strictly 1D.<Box>. Stack has no border/bg/shadow props.<div> you don't need.