Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

Stack, Inline and Grid

The three flow primitives that lay out everything in buttery, and the divider between their children.

Stack, Inline and Grid lay out their children in a column, a row or a grid, with a gap from the spacing scale between them. Use them for every arrangement of siblings, so space belongs to the parent and a component looks the same wherever it's placed.

Choosing one

  • Stack

    For a column: a form's fields, a card's title over its text.

  • Inline

    For a row: a button group, an icon beside its label. wrap lets it wrap, and uniform makes every child as wide as the widest, for a row of equal buttons.

  • Grid

    For columns that change with the room, stepping at 640px and 1024px of the container it's in.

columns={{ base: 1, md: 2, lg: 3 }} gives a Grid one column below 640px of its container, two from 640px and three from 1024px.

All three take align and justify, and render a div unless you pass asChild to lay out an element of your own, such as a ul.

Lists. When you lay out a ul or ol with asChild, give it role="list". The role drops the bullets and the indent, and keeps the list announced as a list in Safari, which drops that along with the bullets.

Stack, Inline and Grid together in a plan picker:

tsx

import type { ReactElement } from "react";
import { Grid, Inline, Stack, Text } from "@butteryui/core";
import { Button } from "@butteryui/react";

export function PlanPicker(): ReactElement {
  return (
    <Stack gap={4}>
      <Text variant="title-md">Choose a plan</Text>
      <Grid columns={{ base: 1, md: 3 }} gap={4}>
        <Text variant="body">Starter</Text>
        <Text variant="body">Roaster</Text>
        <Text variant="body">Wholesale</Text>
      </Grid>
      <Inline gap={2} justify="end">
        <Button variant="soft">Compare plans</Button>
        <Button variant="primary">Continue</Button>
      </Inline>
    </Stack>
  );
}

Divider draws a hairline between children, across a stack or down a row. It's decorative unless it separates groups a screen reader should know about.

Stack

When to use it

Use it for

  • Stacking text, fields or sections vertically
  • Any vertical group whose spacing should come from the scale

Not for

  • Equal-width rows of buttons: use Inline uniform
  • Two-dimensional layouts: use Grid

Examples

Empty state copy

A title and description stacked with the empty-state gap.

Your canvas is empty

Add a slide, or open a deck from your library.

Form fields

A deck's details as label and value pairs: the column keeps its rhythm as a note joins, and each pair keeps its tighter gap.

Title

Spring launch

Slide size

Wide, 1920 × 1080

Variants and states

Gap

The same three boxes at gaps 1, 2, 4 and 6 (4, 8, 16 and 24px).

gap 1
Second
Third
gap 2
Second
Third
gap 4
Second
Third
gap 6
Second
Third

Alignment

Stretch (the default), start, center and end.

stretch
A wider second box
start
A wider second box
center
A wider second box
end
A wider second box

Props

Stack

PropTypeDefaultDescription
align"start" | "center" | "end" | "stretch" | "baseline""stretch"Cross-axis (horizontal) alignment. Defaults to "stretch".
justify"start" | "center" | "end" | "between""start"Main-axis (vertical) distribution. Defaults to "start".
asChildbooleanRender the single child element instead of a div, with Stack's props merged onto it. Give a ul or ol laid out this way role="list" yourself, which drops its bullets and indent.
gapSpaceValueSpace between children.
padSpaceValuePadding on every side.
padXSpaceValueLeft and right padding. Wins over pad on those sides.
padYSpaceValueTop and bottom padding. Wins over pad on those sides.

It also takes ComponentPropsWithRef<"div">.

Accessibility

No role of its own; renders a div (or its child with asChild), so it adds nothing to the accessibility tree.

It takes no keyboard input of its own.

Motion

It doesn't move.

With reduced motion: Nothing changes.

Layout

It's as wide as its parent and as tall as its children and the gaps between them. Children stretch across the column unless align says otherwise, and each can shrink below its content's width, so long text wraps or truncates instead of widening the column. It has no hit area of its own.

Inline

When to use it

Use it for

  • Icons next to labels, button groups, chips in a row
  • Equal-width button pairs with uniform

Not for

  • Toolbars, header bars and nav bars that might run out of room: use OverflowCluster
  • Columns of content: use Stack or Grid

Examples

Equal buttons

A tour's step buttons sharing the width equally, whatever their labels.

Step 3 of 3: Share your deck

Icon and label

An icon and its label with the 6px icon gap.

Copy

Variants and states

Gap

Toolbar clusters use gap 1, button groups gap 2.

gap 1
Two
gap 2
Two

Uniform

Children share the width equally, regardless of their content.

Back
Continue to the next step

Justify

Start (the default), center, end and between.

start
Two
center
Two
end
Two
between
Two

Props

Inline

PropTypeDefaultDescription
align"start" | "center" | "end" | "stretch" | "baseline""center"Cross-axis (vertical) alignment. Defaults to "center".
justify"start" | "center" | "end" | "between""start"Main-axis (horizontal) distribution. Defaults to "start"; ignored when uniform.
uniformbooleanfalseGive every child the same width, such as a dialog's back and forward buttons. Defaults to false.
wrapbooleanfalseLet children wrap onto more lines. Defaults to false. Toolbars, header bars and nav bars never wrap: use OverflowCluster for those.
asChildbooleanRender the single child element instead of a div, with Inline's props merged onto it.
gapSpaceValueSpace between children.
padSpaceValuePadding on every side.
padXSpaceValueLeft and right padding. Wins over pad on those sides.
padYSpaceValueTop and bottom padding. Wins over pad on those sides.

It also takes ComponentPropsWithRef<"div">.

Accessibility

No role of its own; reading order follows source order, so keep the visual and DOM order the same.

It takes no keyboard input of its own.

Motion

It doesn't move.

With reduced motion: Nothing changes.

Layout

It's as wide as its parent and as tall as its tallest child, with its children in one row a gap apart, vertically centered unless align says otherwise. Children can shrink below their content's width, the row wraps only with wrap, and uniform gives every child the same width. It has no hit area of its own.

Grid

When to use it

Use it for

  • Cards in a grid, swatch grids, any set of equal peers

Not for

  • One row of controls: use Inline
  • Page layouts on the 12-column grid: use Page and Section

Examples

Review board

A review board's three columns of decks as equal cards with the 16px card gap: stacked when narrow, three across from 640px.

Draft

3 decks

Review

1 deck

Done

12 decks

Settings pairs

Two equal columns of label and value pairs.

Width

1080 px

Height

1350 px

Variants and states

Columns

Equal columns that never widen for long content.

2 columns with a long label
Cell 2
3 columns with a long label
Cell 2
Cell 3
6 columns with a long label
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6

Props

Grid

PropTypeDefaultDescription
columns1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ResponsiveGridColumns1Number of equal columns, or columns that change with the container's width, such as { base: 1, md: 3 }. Defaults to 1.
align"start" | "center" | "end" | "stretch" | "baseline""stretch"Vertical alignment of items within their row. Defaults to "stretch".
asChildbooleanRender the single child element instead of a div, with Grid's props merged onto it.
gapSpaceValueSpace between children.
padSpaceValuePadding on every side.
padXSpaceValueLeft and right padding. Wins over pad on those sides.
padYSpaceValueTop and bottom padding. Wins over pad on those sides.

It also takes ComponentPropsWithRef<"div">.

ResponsiveGridColumns

Columns that change with the space the grid has: `base` below 640px of its container, `md` from 640 and `lg` from 1024 (the page's breakpoints). A size left out keeps the one below it.

FieldTypeDefaultDescription
base1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 121Columns below 640px. Defaults to 1.
md1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12Columns from 640px.
lg1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12Columns from 1024px.

Accessibility

No role of its own; items are read row by row in source order.

It takes no keyboard input of its own.

Motion

It doesn't move.

With reduced motion: Nothing changes.

Layout

It's as wide as its parent, split into equal columns that can each shrink to nothing, so one long item never widens its column. Given an object, the columns change at 640px and 1024px of the nearest size container. Each row is as tall as its tallest item, which the others stretch to unless align says otherwise. It has no hit area of its own.

Divider

When to use it

Use it for

  • Separating groups in a menu, toolbar or header
  • Between rows that aren't already divided by their card

Not for

  • Spacing groups apart: use the parent's gap
  • Rows inside an inspector card: the card draws their dividers

Examples

Grouped list

A deck's slides and who it's shared with in one card, split into two groups by a divider.

Title slideQ3 in numbersShared with the design team

Header separator

A header's title and save status, split by the 24px vertical divider, with the save action at the end.

Spring launch
Unsaved changes

Variants and states

Horizontal

Between groups in a column.

AboveBelow

Vertical

Full height by default, or a length from the spacing scale.

Fulllength 6length 4End

Props

Divider

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal""horizontal" (the default) runs across a column; "vertical" separates items in a row.
lengthSpaceValueLength of a vertical divider on the spacing scale, such as 6 for the 24px separator in a header. Without it the divider spans the row's full height.
decorativebooleanfalseHide the divider from screen readers when it's purely visual. Defaults to false, which announces it as a separator.

It also takes Omit<ComponentPropsWithRef<"div">, "children">.

Accessibility

Announced as a separator with its orientation; decorative dividers use role="none" and are skipped.

It takes no keyboard input of its own.

Motion

It doesn't move.

With reduced motion: Nothing changes.

Layout

A horizontal divider runs across its column's full width; a vertical one runs its row's full height, or the length you give it on the spacing scale, centered, such as length={6} for 24px. Either way it takes up only its 1px line, never growing or shrinking with the space around it, and it has no hit area.

  • The layout contract

    The 13 rules every buttery screen follows, so nothing overlaps, overflows or shifts, and how they're checked.

  • Spacing and sizing

    The 4px spacing scale, the control heights that change with density, and the widths pages and panels keep to.