Stack

A vertical column of blocks a fixed gap apart, and Section for a titled group.

Overview

Any blocks that follow each other: space is a Stack's gap, never a margin on a block.

Gap

Do

Build 412

3 changed

Build 411

No changes
import { Body, Card, Stack } from "@hackcove/ui";

<Stack>
  <Card title="Build 412">
    <Body>3 changed</Body>
  </Card>
  <Card title="Build 411">
    <Body>No changes</Body>
  </Card>
</Stack>
Blocks that follow each other go in a Stack: the gap is one step of the scale, md by default.
Don't

Build 412

3 changed

Build 411

No changes
import { Body, Card } from "@hackcove/ui";

<div>
  <Card title="Build 412">
    <Body>3 changed</Body>
  </Card>
  <Card title="Build 411">
    <Body>No changes</Body>
  </Card>
</div>
Blocks in a bare div touch each other. Space is the Stack's job, never left to the blocks.

Section title

Do

Recent builds

Build 412 · 3 changed
Build 411 · no changes
import { Body, Button, Card, Section } from "@hackcove/ui";

<Section title="Recent builds" actions={<Button variant="outline">All builds</Button>}>
  <Card>
    <Body>Build 412 · 3 changed</Body>
  </Card>
  <Card>
    <Body>Build 411 · no changes</Body>
  </Card>
</Section>
A titled group is a Section: the title, the actions beside it and the blocks under it, as one named region.
Don't

Recent builds

Build 412 · 3 changed
Build 411 · no changes
import { Body, Button, Card, Cluster, SectionHeading, Stack } from "@hackcove/ui";

<Stack gap="sm">
  <Cluster justify="between">
    <SectionHeading>Recent builds</SectionHeading>
    <Button variant="outline">All builds</Button>
  </Cluster>
  <Card>
    <Body>Build 412 · 3 changed</Body>
  </Card>
  <Card>
    <Body>Build 411 · no changes</Body>
  </Card>
</Stack>
A heading row built by hand is not tied to its group: nothing names the region and the row's spacing is a guess. Use Section.

List

Do
  • Build 412 · 3 changed
  • Build 411 · no changes
import { Body, Card, Stack } from "@hackcove/ui";

<Stack as="ul" gap="sm" aria-label="Builds">
  <li>
    <Card>
      <Body>Build 412 · 3 changed</Body>
    </Card>
  </li>
  <li>
    <Card>
      <Body>Build 411 · no changes</Body>
    </Card>
  </li>
</Stack>
A list of things is Stack as="ul" with an li per item: the spacing of a Stack and a list that assistive tech announces.
Don't
Build 412 · 3 changed
Build 411 · no changes
import { Body, Card, Stack } from "@hackcove/ui";

<Stack gap="sm">
  <Card>
    <Body>Build 412 · 3 changed</Body>
  </Card>
  <Card>
    <Body>Build 411 · no changes</Body>
  </Card>
</Stack>
The same cards in a plain Stack are two unrelated blocks: no list, no count, nothing to jump by.

Usage

  • gap is a scale: xs between lines of text, sm inside a group, md (default) between blocks, lg between groups, section between a page's top-level blocks (the one responsive page rhythm).
  • width caps the column at the left edge: narrow (a reading measure), wide, full, or default for the parent's width. It is never re-centred.
  • as is div (default), section, form, ul or ol. as="form" takes the form props and spaces its fields; as="ul" and as="ol" draw no markers and their children are <li>s.
  • A titled group is a Section: title, actions beside it, then the blocks, at gap sm. It takes gap and width as Stack does.
  • A row of items is a Cluster, not a Stack.

Accessibility

  • Section is a <section> named by its title (aria-labelledby), so it is a region; with no title give it an aria-label.
  • A Stack with as="ul" or as="ol" is a real list: give it an aria-label when more than one list is on the page.
  • as="form" is a <form>; give it an aria-label or let a heading name it.
  • A div Stack adds no role.

API

Stack

A vertical column of blocks a fixed gap apart: xs between lines of text (a heading and the line under it), sm inside a group, md (default) between blocks, lg between groups, section between a page's top-level blocks (gap-12, sm:gap-16: the one responsive page rhythm). width caps the column: narrow is a reading measure (max-w-3xl), wide is max-w-5xl, full is the parent's whole width and default sets nothing. A capped Stack starts at its parent's left edge (it is never re-centred), so its first line lines up with every other block on the page. as is a closed set: div (default), section, form, ul or ol. A form takes the form props (onSubmit, action, method, noValidate, ...). A ul or ol is drawn with no markers and its children are <li>s, as in BulletList and Timeline: the Stack does not wrap them, so a child is one list item and Body as="li" or a bare <li> both work.


as
Type
"form" | "div" | "ol" | "section" | "ul"
Default
div

gap
Type
"sm" | "md" | "lg" | "section" | "xs"
Default
md

width
Type
"default" | "narrow" | "full" | "wide"
Default
default

Section

A <section> Stack with an optional SectionHeading title (which names it) and actions beside it.


actions
Type
ReactNode

gap
Type
"sm" | "md" | "lg" | "section" | "xs"
Default
sm

title
Type
ReactNode

width
Type
"default" | "narrow" | "full" | "wide"