Stack
Arrange inline elements or cards in a flex row or column with token gaps, dividers and mobile overrides.
On this page
Stack is a flexbox container for a row of inline elements (buttons, chips, icons) or a row of cards. It sets direction, alignment, wrapping and a token gap from props, and can draw hairlines between its children. Headings, paragraphs and block components belong directly inside a Section, Card or Column instead, where the rich-text rhythm spaces them. See Spacing model.
import { Button, Chip, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<Stack direction="col" gap="md" alignItems="center">
<Stack gap="xs" wrap justifyContent="center">
<Chip.span>Design</Chip.span>
<Chip.span>Development</Chip.span>
<Chip.span>Content</Chip.span>
</Stack>
<Stack gap="sm" alignItems="center" wrap justifyContent="center">
<Button.a href="#" variant="primary">
Start a project
</Button.a>
<Button.a href="#" variant="secondary">
See our work
</Button.a>
</Stack>
</Stack>
)
}ImportLink to this section
import { Stack } from '@systhemaui/next'In a React app without Next.js, import it from @systhemaui/react.
ExamplesLink to this section
Direction and alignmentLink to this section
direction is row (the flex default), row-reverse, col or col-reverse. justifyContent (start, end, center, between, around, evenly) and alignItems (start, end, center, baseline, stretch) map to the matching flex utilities.
import { Button, Paragraph, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<Stack
className="w-full max-w-xl"
justifyContent="between"
alignItems="center"
gap="md"
wrap
>
<Paragraph type="lead">Ready to start?</Paragraph>
<Button.a href="#" variant="primary">
Book a call
</Button.a>
</Stack>
)
}GapsLink to this section
gap takes a size from the token gap scale (none, xs, sm, md, lg, xl, 2xl, 3xl), or { x, y } with a size per axis. The values come from --gap-*, which change per breakpoint with the default tokens. See Gap.
import { Chip, Stack } from '@systhemaui/next'
const tags = ['Branding', 'Web design', 'Copywriting', 'SEO', 'Photography', 'Analytics', 'Hosting']
export default function Demo() {
return (
<Stack className="max-w-md" gap={{ x: 'xs', y: 'lg' }} wrap>
{tags.map((tag) => (
<Chip.span key={tag}>{tag}</Chip.span>
))}
</Stack>
)
}DividersLink to this section
divider draws a 1px hairline in --color-separator-line between adjacent children: vertical in a row, horizontal in a column. It is never drawn before the first or after the last child. Pass a node instead of true to use your own separator.
import { Paragraph, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<Stack direction="col" gap="xl" alignItems="center">
<Stack direction="col" gap="sm" divider className="w-72">
<Paragraph>Design review</Paragraph>
<Paragraph>Content migration</Paragraph>
<Paragraph>Launch checklist</Paragraph>
</Stack>
<Stack
gap="sm"
alignItems="center"
divider={
<Paragraph type="label" aria-hidden="true">
/
</Paragraph>
}
>
<Paragraph type="small">Home</Paragraph>
<Paragraph type="small">Journal</Paragraph>
<Paragraph type="small">Field notes</Paragraph>
</Stack>
</Stack>
)
}Mobile overridesLink to this section
mobileOverrides takes direction, justifyContent, alignItems, wrap and gap again, applied below the md breakpoint (max-md: variants). Here the cards sit in a row on desktop and stack on a phone; switch the preview viewport to compare.
import { Card, Heading, Paragraph, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<div className="container py-12">
<Stack gap="lg" mobileOverrides={{ direction: 'col', gap: 'sm' }}>
<Card variant="default" className="flex-1">
<Heading.h4>Discovery</Heading.h4>
<Paragraph>Two workshops to agree on goals and scope.</Paragraph>
</Card>
<Card variant="highlighted" className="flex-1">
<Heading.h4>Delivery</Heading.h4>
<Paragraph>Weekly releases you can review on a preview site.</Paragraph>
</Card>
</Stack>
</div>
)
}Semantic listsLink to this section
as renders the stack as another element. Use as="ul" with <li> children for a list of links or tags, so screen readers announce the item count.
import { Link, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<Stack as="ul" gap="md" wrap justifyContent="center">
<li>
<Link href="#">Services</Link>
</li>
<li>
<Link href="#">Case studies</Link>
</li>
<li>
<Link href="#">Contact</Link>
</li>
</Stack>
)
}Child animationsLink to this section
A Stack reveals as one unit: core's .stack rule cancels every scroll reveal below it, at any depth. When the Stack holds a list of independent items that should reveal one by one (a posts listing does this), set allowChildAnimation. It adds the aos-allow-children marker and no animation of its own. disableAnimation removes the Stack's own reveal.
PropsLink to this section
wrap: passwrap(orwrap={true}) to let items wrap, or'wrap-reverse'/'nowrap'.grow,shrinkandinlineaddgrow,shrinkandinline-flex(instead offlex).mobileOverridestakesdirection,justifyContent,alignItems,wrapandgap, with the same values as the props of the same name.
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | - | |
children | ReactNode | - | |
className | string | - | |
direction | FlexDirection | - | |
justifyContent | JustifyContent | - | |
alignItems | AlignItems | - | |
wrap | boolean | FlexWrap | - | |
gap | StackGap | - | |
grow | boolean | - | |
shrink | boolean | - | |
inline | boolean | - | |
divider | ReactNode | - | Render a hairline separator between adjacent children — handy for post lists, search results, and archive rows. - true renders a 1px --color-separator-line hairline, oriented to match direction (vertical between row/inline items, horizontal between column items). - A ReactNode renders that custom node between children instead. No-op when there's only one child, and never rendered before the first or after the last child. |
disableAnimation | boolean | - | |
allowChildAnimation | boolean | - | Let the Stack's CHILDREN keep their own scroll-reveal animations. A Stack normally reveals as one composed unit, so core's .stack CSS cancels every .aos / .animates-on-scroll reveal below it, at any depth. That is right for a laid-out group and wrong for a Stack that holds a LIST of independent things (a posts listing, whose cards are meant to reveal one by one as they scroll into view). Setting this adds the aos-allow-children marker, which core's rule excludes (.stack:not(.aos-allow-children)). It does not add any animation of its own — it only stops the Stack from cancelling the children's. Defaults to false, so existing markup is unaffected. |
mobileOverrides | { direction?, justifyContent?, alignItems?, wrap?, gap? } | - | |
…and all <div> attributes |
HTML and CSSLink to this section
Stack composes Tailwind flex utilities with the token gap classes:
<div class="stack flex flex-col items-center gap-md max-md:flex-row">
<a class="button-primary" href="/start"><span class="button-title">Start a project</span></a>
<span aria-hidden="true" class="stack-divider w-full"></span>
<a class="button-secondary" href="/work"><span class="button-title">See our work</span></a>
</div>A hand-written divider needs its own size and color (height: 1px; background-color: var(--color-separator-line)); the component sets them inline. The .stack class carries the reveal rule:
| Class | Styles |
|---|---|
stack | :is(.richtext > :is(.columns, :is(.stack), .card-default, .card-highlighted, .accordion-default)):not(:first-child) { margin-top: calc(var(--typography-block-margin-y, 24px) * 2); }:is(.richtext > :is(.columns, :is(.stack), .card-default, .card-highlighted, .accordion-default)):not(:last-child) { margin-bottom: calc(var(--typography-block-margin-y, 24px) * 2); }+ 1 more rule |
The gap values come from the token gap scale.
Next.jsLink to this section
@systhemaui/next re-exports Stack from @systhemaui/react unchanged.
AccessibilityLink to this section
Dividers drawn with divider are aria-hidden; a custom divider node is announced, so mark it aria-hidden="true" when it is decorative. Flex *-reverse directions change the visual order but not the reading or tab order, so keep the source order meaningful. See Child reveals for the animation rule.