Brik Design System
Build Standards

Content Rhythm

Space content by role, not by eye — one escalating scale from tight text pairings to section breaks, and when to reach for gap, padding, or margin.

Every vertical gap between content — a heading and its subtitle, two paragraphs, one block and the next, one section and the next — comes from a single escalating scale keyed to the relationship between the two elements, not from a per-page judgment call. Pick the step by what the elements are to each other; the token re-modulates automatically under each spacing mode, so the rhythm holds across compact product UI and spacious marketing pages without re-tuning.

The rhythm scale

Tighter means more coupled. As the relationship loosens — words in a heading pair, sentences in a paragraph, blocks in a section, sections on a page — the step widens by one notch.

RelationshipIntentTokenBase
Title ↔ subtitle / eyebrowTightly coupled — reads as one unit--gap-sm6px
Heading / subtitle ↔ bodyReadable separation--gap-md8px
Paragraph ↔ paragraphDistinct thoughts--gap-lg16px
Block ↔ blockSeparate content units--gap-xl24px
Section ↔ sectionPage-level breakpadding-block: clamp(--padding-xl, 7vw, --padding-huge)32–48px, fluid

Never use --gap-xs or --gap-tiny in any vertical rhythm position — content or component. Both collapse to 0px outside the default spacing mode, which erases the step entirely. And never hardcode a px value: the scale re-modulates per mode, a literal does not. The lint-content-rhythm gate enforces both rules on component CSS.

Component rhythm

The same discipline extends one layer up: vertical space between components is also keyed to the relationship, not to the component (ADR-024).

RelationshipIntentTokenBase
Label ↔ the control it namesReads as one unit — meter label ↔ bar, stacked field label ↔ value--gap-sm6px
Row ↔ row in a dense containerMenu options, list items, bullet items (comfortable density steps up one)--gap-sm6px
Control ↔ control in a clusterFooter actions, a filter-chip row--gap-md8px
Field ↔ field in a formDistinct inputs, one flow--gap-lg16px
Card ↔ card in a grid or listSeparate bounded units--gap-lg16px
Banner ↔ banner in a groupA queue the reader clears, not peers they compare — one step tighter than cards so the stack reads as one notice block (banner groups)--gap-md8px
Container ↔ containerSame as block ↔ block above--gap-xl24px

Layout primitives (Stack, Grid, Cluster) stay generic — they can't know which relationship they're arranging, so this table governs which gap value you pass, not the primitive. Horizontal gaps (icon ↔ label, chips in a row) are outside this table; --gap-xs remains legitimate there.

Gap, padding, or margin

The scale above answers how much. This answers with which property — and the answer shifts as you move up the layers.

MechanismControlsReach for it whenToken family
gap (flex / grid)Uniform space between sibling elementsOne repeating step between peers — blocks in a Stack, cells in a Grid--gap-*
Adjacency margin (a + b)Exact space between two specific elementsEach pair needs a different step — the text slots inside a Block, elements inside Prose--gap-*
paddingInset from a container's own edgeSpace between content and the box boundary; a section's vertical band--padding-*

The distinction the layers turn on: gap is between siblings; padding is the edge. A section break is padding, not gap — each section owns its own padding-block band, sections don't sit in a gapped flow. So block-to-block spacing inside a section is the layout primitive's gap, but section-to-section spacing is each section's padding.

Adjacency margin covers the one case a single flex gap cannot: a Block's title → subtitle is --gap-sm while its subtitle → description is --gap-md — two different steps between three stacked elements. ContentBlock sets each with margin-top rather than one uniform gap (ADR-023); Prose spaces h + p and p + p differently for the same reason.

Adjacency margin is the only place margin sets vertical rhythm. Never use margin to space one component from another — that space belongs to the parent layout's gap.

The section separator

The section component owns the separator between sections. A hand-placed <Divider /> between two sections is duplicate chrome — the line is already there.

On a product page, consecutive DataSection siblings draw their own rule and their own space from one adjacency selector: --padding-lg of margin-top, the same again as padding-top, and a --border-width-md --border-muted top border. That is the identical token treatment <Divider /> renders, so placing one between two DataSections paints the line twice. SheetSection takes the spacing half of the same rule (margin-top: var(--padding-lg)) and deliberately draws no border — a sheet body is already a bounded surface.

BetweenSeparator comes fromHand-place a Divider?
Two DataSection siblingsThe adjacency rule on DataSectionNo — it is already drawn
Two SheetSection siblingsThe adjacency rule on SheetSection, spacing onlyNo — a sheet body needs no rule
A section and a non-section blockNothing — the sibling selector does not matchYes, this is the one correct case

The sibling selector is what makes this automatic and what makes the exception real. Insert any non-section element between two sections and the adjacency breaks, so the second section loses its rule — that gap is the only place a hand-placed Divider belongs. If you find one anywhere else, delete it rather than tuning the space around it.

Where each step is set

You rarely set these values by hand — the standard is baked into the layer that owns each relationship.

  • Text within a blockContentBlock (named slots) or Prose (free HTML) own it. See Content blocks and prose.
  • A section intro (centered title + description)SectionHeader. It composes ContentBlock for this rhythm and adds the one thing rhythm alone doesn't: the horizontal measure that caps and centers the text column so intros stop being 600px on one page and full-width on the next. See Content measure.
  • Block ↔ block → the layout primitive's gap (Stack, Grid) — --gap-xl in a section's content region.
  • Section ↔ section → the section shell's padding-block clamp, harmonised to 7vw (ADR-021). A section needing a different band re-declares padding-block on its own selector.
  • Display Choice — which display holds the content this page then spaces.
  • Spacing — the --gap-* / --padding-* token scales and the density modes that modulate them.
  • Composition Layers — the Section → Layout → Container → Block or Control → Component model these adjacencies span.
  • Headings — the heading roles these steps sit above.
  • Paragraphs — the body roles the paragraph steps space.
  • Page Grid — the horizontal axis: the width-container recipe and the page gutter.
  • Page Structure — the HTML skeleton the rhythm applies within.
  • Typography — the type scale for the content being spaced.

On this page

💬