Docs
Next

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.

Row and column gaps
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.

Mobile overrides
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.

Stack as a list
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: pass wrap (or wrap={true}) to let items wrap, or 'wrap-reverse' / 'nowrap'.
  • grow, shrink and inline add grow, shrink and inline-flex (instead of flex).
  • mobileOverrides takes direction, justifyContent, alignItems, wrap and gap, with the same values as the props of the same name.
PropTypeDefaultDescription
asElementType-
childrenReactNode-
classNamestring-
directionFlexDirection-
justifyContentJustifyContent-
alignItemsAlignItems-
wrapboolean | FlexWrap-
gapStackGap-
growboolean-
shrinkboolean-
inlineboolean-
dividerReactNode-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.
disableAnimationboolean-
allowChildAnimationboolean-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:

ClassStyles
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.