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.
Configure <Stack> defaults application-wide through the theme's components field.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Stack: {
defaults: { direction: 'col', gap: 4, align: 'stretch' },
},
},
});Configurable axes (StackVariantProps):
| Axis | Type | Notes |
|---|---|---|
direction | 'row' | 'col' | Flex direction. |
align | 'start' | 'center' | 'end' | 'stretch' | 'baseline' | Cross-axis alignment. |
justify | 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | Main-axis alignment. |
gap | 0 | 0.5 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16 | 24 | Token-scale spacing between children. |
wrap | boolean | Enable flex-wrap. |
fullWidth | boolean | Stretch to container width. |
fullHeight | boolean | Stretch to container height. |
Precedence chain (lowest → highest):
defaultVariants from the internal stackVariants recipe (direction: 'col').theme.components.Stack.defaults — application-wide override.<Stack direction="row" />) — wins over theme.sx — wins over the resolved variant class via tailwind-merge.useComponentDefaults('Stack') reactively re-renders every mounted Stack when patchTheme runs at runtime.
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'stretch' | 'baseline' | — | Cross-axis alignment (items-*). |
as | ElementType | — | Override the rendered HTML tag. Defaults to 'div'. Use when the Stack also has semantic meaning — <Stack as="nav"> for a nav bar, <Stack as="ul"> for a list (children become <li> via native HTML, not by us).Ignored when asChild is true. |
asChild | boolean | — | Render via Radix Slot — the Stack styles paint onto the single React child instead of wrapping it. Mutually exclusive with as (when both are passed, asChild wins). The divider prop is ignored when asChild is true (the Slot pattern wraps a single node, so the N-1 insertion logic has no place to act). |
direction | 'row' | 'col' | 'row-reverse' | 'col-reverse' | 'col' | Flex direction. |
divider | ReactNode | — | Node rendered N-1 times BETWEEN children — mirror of MUI Stack's divider prop. Useful for visual separators (a <hr>, a thin <Box variant="outlined" sx="h-px border-0 border-t">, an <svg> glyph) that need to follow the flex direction.The node is cloned for each insertion via cloneElement; pass a stable element (not a function) for best React reconciliation. React.Children.toArray flattens fragments before the walk, so <Stack divider={...}><><...></></Stack> works as expected. |
fullHeight | boolean | false | Stretch to fill the container's height. |
fullWidth | boolean | false | Stretch to fill the container's width. |
gap | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Gap between children — spacing token step. |
justify | 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | — | Main-axis alignment (justify-*). |
sx | string | — | Utility classes appended to the variant chain. Resolved via tailwind-merge so the consumer's classes always win over the variant defaults. |
wrap | boolean | false | Allow children to wrap to the next line (flex-wrap). |
...rest | Omit<HTMLAttributes<HTMLDivElement>, 'className'> | — | Additional native attributes forwarded via Omit<HTMLAttributes<HTMLDivElement>, 'className'>. |
<Stack> has no slots. It renders a single flex-column (or row) DOM element wrapping its children — there is no anatomy to target separately. The optional divider prop injects a node between children but does not carry a slot API.
The slotProps API pattern only exists on compound components with multiple visually distinct regions. Examples: <Menu>, <DataGrid>, <Autocomplete>.
For <Stack>, use sx on the root; style your children directly (they're your own components).
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.