Composition Layers
Six conceptual layers — Section, Layout, Container, Block, Control, Component — and the decision rules for assembling them.
A page in BDS is composed in six layers. Each has a single responsibility, a single vocabulary, and only knows about the layer below it. Block and Control sit at the same depth — both are fixed-slot shapes one step above Component; one holds content, the other holds controls. This model is pedagogical — it explains how to think about a page — and is intentionally non-binding on the BEM canon (ADR-008).
The layers
| Layer | Responsibility | Vocabulary | Examples |
|---|---|---|---|
| Section | Page role with surrounding structure (vertical rhythm, container, background surface) | Page-level semantic | Hero, Content, CTA |
| Layout | Pure composition primitive — arranges children. No styling beyond structure. | Composition | Stack, Cluster, Grid, Split, Row |
| Container | Styled holder that composes blocks into a self-contained unit. Carries border / padding / elevation / radius. | Bounded unit | Card, List, Form, Accordion, Tabs |
| Block | Composed content unit — fixed slot shape filled with atoms. | Slot + atoms | ContentBlock, MediaBlock, ListItem, FormField, Stat |
| Control | Interactive bar that operates on an adjacent display or heads a page. Fixed slot shape filled with controls, not content. | Slot + controls | FilterBar, PageHeader, TabBar, SubNavigation, Pagination |
| Component | Single primitive atom. | One primitive | Button, Input, Image, Badge, Icon |
Where each layer lives
| Layer | Directory | Notes |
|---|---|---|
| Section | content-system/blueprints/{react,astro}/ | Blueprint families per ADR-008 play the Section role |
| Layout | components/ui/ | Stack, Cluster, Grid, Split, Row |
| Container | components/ui/ | Card, Accordion, List |
| Block | components/ui/ | Field, Card preset="summary" |
| Control | components/ui/ | FilterBar, PageHeader, TabBar, SubNavigation, Pagination |
| Component | components/ui/ | Atomic primitives |
Block vs container
If the thing is described primarily by how its children are styled and bounded (border, elevation, padding, max-width) → it's a container. If it's described by what slots it offers and which atoms fill them (heading, body, kicker, action) → it's a block.
Card passes "styled and bounded" → container. ContentBlock passes "slots and atoms" → block.
Card is a styled container
A Card encodes only its container styling (border, radius, padding, elevation). Orientation comes from the layout primitive inside it, not a Card variant.
// ✅ Stacked card — Stack layout inside Card
<Card>
<Stack>
<MediaBlock />
<ContentBlock />
</Stack>
</Card>
// ✅ Horizontal card — Split layout inside Card
<Card>
<Split>
<MediaBlock />
<ContentBlock />
</Split>
</Card>
// ❌ Don't bake orientation into Card
<Card variant="horizontal">...</Card>
<Card layout="stacked">...</Card>This keeps CardImageLeft, CardStacked, CardHorizontal from existing — they're all "Card + a layout child."
Stat is a block
Stat (value + label, two slots, no border styling) is a block, not a container. The bordered "stat tile" look is a Card containing a Stat.
// Bordered stat tile
<Card>
<Stat value="75%" label="of website credibility comes from design" />
</Card>
// Card with content + metric
<Card>
<ContentBlock title="First impressions" body="..." />
<Stat value="0.05s" label="to make a first impression" />
</Card>If you find yourself reaching for a StatCard, you're conflating block and container — write it as Card + Stat instead.
Control vs block and container
If the thing is described by which atoms fill its slots → it's a block. If it's described by what it does to something else on the page (filter, paginate, switch, act) → it's a control.
A control never holds the content it governs. FilterBar heads the table it filters; that table is its sibling, not its child. Pagination pages a list it does not contain. TabBar switches a panel it knows nothing about. That is the line against Container: a container bounds its children, a control operates on a neighbour and bounds nothing.
The line against Block is subtler, because both are fixed-slot shapes at the same depth. Ask what the slot content is about:
- A
ContentBlockwith a<Button>in its action slot is still a block — the button acts on the block's own content. - A
FilterBarwith a title in its title slot is still a control — the title names the display underneath it.
Reaching for a Cluster with justify="between" to build a bar is the tell that a control is missing. Cluster is a Layout primitive: it arranges, it carries no vocabulary, and every hand-rolled instance diverges. Use the control, or file for one.
Control bar or section header
A control bar and a section header both render a title at --heading-sm, so the rendered output gives you no signal about which produced it. Pick by what the title is over:
| The title heads | Reach for | Component |
|---|---|---|
| A display — table, list, board, card grid | A control bar | FilterBar |
| A record — fields in read or edit mode | The section container's own header | DataSection title, SheetSection heading |
| The whole page | The page header | PageHeader |
// ✅ Collection — the control bar heads the display it filters
<FilterBar title="Service plans" label="plans" total={42} filtered={12}>
<FilterButton ... />
</FilterBar>
<Table ... />
// ✅ Record — the section container owns its own header
<DataSection title="Billing details">
<FieldGrid>
<Field label="Plan">Growth</Field>
</FieldGrid>
</DataSection>
// ❌ Don't head a record with a control bar
<FilterBar title="Billing details" label="fields" total={6} filtered={6} />
<FieldGrid>...</FieldGrid>
// ❌ Don't hand-roll a control bar out of a section header
<DataSection title="Service plans" actions={<Button>Clear filters</Button>}>
<Badge>12 of 42</Badge>
<Table ... />
</DataSection>The last case is the one that ships. A section header plus a hand-placed count and clear button is a FilterBar reimplemented without its collapse behaviour, its counter status, or its accessible name — the mixture found on one product page in brik-client-portal, where bds-data-section__header, bds-filter-bar, and bds-cluster--justify-between all did this job at once.
SectionHeader is not the section header this rule means. It is the centered marketing section intro (title + description + content measure), not a record header. On a product page the record header is the section container's own title slot.
Container reference
| Container | Purpose | When |
|---|---|---|
DataSection | Titled block of read-mode data on a page | Overview / profile tabs, client-detail pages |
SheetSection | Titled block inside a sheet body (uppercase label heading) | Any grouping inside <Sheet> |
Card (and variants) | Bordered, self-contained content unit | Grids of comparable items, dashboards, marketing |
Board (BoardColumn, BoardCard) | Kanban-style container | Task boards |
Dialog / Modal / Sheet | Overlay containers | Focused interactions |
Don't reach for a Card when a DataSection is right. Cards are self-contained units in a grid; DataSection is one region of a larger page.
Identify the layer before you target it
When a change names an element ("the card", "the section", "the button"), find the target by its layer, reading the DOM top-down (Section → Layout → Container → Block or Control → Component) — never by selector-name resemblance.
A BEM block name identifies the blueprint family; it does not define the element's layer. A block whose name contains "card" can still be a Section. The Card is whichever nested element plays the Container role.
bp-hero-img-card is a Section (<section>). The Card is its nested Container, aside.bp-hero-img-card__media-card. They own different surfaces: the Section carries the page-role/band surface; the Container (Card) carries its own bounded surface (its --bds-hero-img-card-media-bg hook). A surface meant for a service-identified card — e.g. ADR-020's --surface-service-{line}-inverse — is a Container surface; applying it to the Section repaints the whole band, not the card. (Regression caught in brikdesigns#637.)
The block-vs-container decision rule above answers "what layer is this?"; apply it to the element you're about to change, not to the name that looks closest.
Related
- Naming Principles — the slot pattern and blueprint naming rules
- Page Structure — how sections are structured in HTML
- Content Rhythm — the spacing scale between these layers, from text pairings to section breaks, and which layer owns the separator between sections
- Display Choice — which display a Control layer bar heads
- Page Archetypes — the four assembled product pages these layers compose into
- Theming — Blueprints — blueprint families that play the Section role