ADR 0006 — Component classification by functional role
Status
Accepted, 2026-05-18.
Source: v1.0 architectural quiz outcomes
(docs-internal/architecture/v1-master-diff-merge.md §2). This ADR
formalizes the decision; the master diff-merge remains the operational
reference.
Context
The v0.1.0 model used three component groups — Elements, Blocks, Overlays — plus an informal notion of Sections via examples. In practice this caused recurring categorization disputes:
- Avatar is called from many components (Card, Header, Comment lists). It has no own semantics, no slots, no logic. Treating it as an "Element" lumps it with Button (which has a behavior contract) and Input (which has form-state contract).
- Tabs has fixed structure (tab list + panels) and does not accept arbitrary content. Classifying it as a Block (alongside Card and Form, which both accept arbitrary children) obscured its real shape.
- Alert has the same fixed-structure property: icon + title + message + up to two buttons. As a Block it implied composability it doesn't have.
- Sections had no first-class home — example pages composed them ad-hoc as Blocks, which broke the "Blocks compose into pages" intuition.
Without explicit functional criteria, the next ~40 v1.0 components (Spinner, Skeleton, Field-group, Site-header, Hero, etc.) would each trigger fresh categorization debates.
Decision
Components are classified into four functional layers, plus a preserved Overlays layer:
| Layer | Test | Examples |
|---|---|---|
| Foundation | No slots, no logic, called from many components, no own semantics | Avatar, Icon, Typography, Divider, Spinner, Skeleton, Dots, Image |
| Element | Fixed structure, takes props, doesn't accept arbitrary content | Button, Input, Tabs, Badge, Alert, Tag, Kbd |
| Block | Slots for elements; structural brick for sections | Card, Accordion, Carousel, Breadcrumb, State |
| Section | Slots for blocks; organizes a page region | Hero, CTA, Site-header, Site-footer, layouts |
| Overlay | Same component identity, but rendered in portal/top-layer | Dialog, Sheet, Tooltip, Popover, Toast |
The test is what the component does, not how complex it looks. A visually elaborate Foundation (Spinner with multi-ring animation) remains a Foundation because it has no slots and no semantics. A visually simple Block (a one-line Quote block) remains a Block because it has slots.
Element sub-categories
Elements subdivide into three folders by domain:
elements/interactive/— actionable controls (Button, Button-group, Tabs, Switch, Toggle, Kbd)elements/form/— form field primitives and form-aware variants (Field-group, Label, Input, Textarea, HelperText, Error, Select, Radio-group, Checkbox, Checkbox-group, Form-button, Form-divider)elements/display/— non-interactive display primitives (Badge, Tag, Tags-group, Avatar-group, Code, Alert)
The sub-categorization is for file organization and discoverability. Lint rules and composition rules apply at the layer level, not the sub-category level.
Overlays preserved as a layer
Overlays are conceptually a presentation mode applied to existing
components (a Dialog is a "Card in modal mode"). We considered
collapsing them into a data-mode="overlay" modifier on Blocks. We
rejected this because:
- Developers reach for "Dialog" or "Sheet" as a category, not as a modifier of something else.
- Overlays have distinct technical concerns (focus trap, top-layer, z-index management, escape handling) that warrant a dedicated runtime module.
- Documentation discoverability is materially better with a dedicated Overlays section than with an "Overlay mode" footnote on each candidate Block.
We preserve the mode concept in architectural discussions and AGENTS.md ("Toast and Notifications are overlay presentations of the same idea as inline Alert") but organize file structure and documentation as a discrete layer.
Composition rules
- A Foundation may use only HTML primitives and tokens. Foundations do not import other components.
- An Element may use Foundations.
- A Block may use Elements and Foundations. Blocks may compose other Blocks of strictly lower complexity (no cycles).
- A Section may use Blocks, Elements, and Foundations. Sections may rarely compose other Sections (e.g. Hero inside a multi-section page is application-level composition, not Section-of-Section).
- An Overlay may use Elements, Foundations, and (sparingly) Blocks. Overlays do not compose into other Overlays — stacking is an application concern.
Circular composition is forbidden across layers. The linter
(@malevich/lint) detects circular imports across layer folders.
Migration impact (v0.1.0 → v0.2.0)
Three existing components move:
elements/avatar/→foundations/avatar/. No public API change; consumers re-import.blocks/tabs/→elements/interactive/tabs/. No public API change.blocks/alert/→elements/display/alert/. No public API change.
The migration is a file move + import-path update. A codemod ships with v0.2.0 to update consumer imports automatically.
Consequences
Positive:
- Every future component decision routes through a single functional test, not a category vote.
- Foundation/Element/Block/Section maps cleanly to documentation IA: one page per layer with consistent navigation.
- File organization mirrors mental model — a contributor looking for Spinner finds it under Foundations on first try.
- Lint rules can enforce composition direction reliably.
- Migration cost is bounded: three components move, no API breaks.
Negative / tradeoffs:
- Existing v0.1.0 examples and docs reference the old paths. Every reference must be updated as part of v0.2.0.
- Some judgement calls remain ambiguous (e.g. is "Quote block" a Block or a Foundation?). The Block-vs-Foundation test is "has slots" — if the component takes children via slot, it is a Block.
- Element sub-categorization adds an extra directory level; contributors must learn the interactive/form/display split.
Alternatives considered
- Keep three groups, document conventions. Rejected: every new component re-opens the categorization debate. The categorization is load-bearing — it deserves explicit rules.
- Collapse Foundations into Elements. Rejected: it merges primitives with no own semantics (Spinner) with primitives that carry behavior contracts (Button). Different lifecycle, different documentation needs.
- Collapse Overlays into Blocks with a
modemodifier. Rejected: see "Overlays preserved as a layer" above. - Use a flat component list with tags instead of folders. Rejected: file system is the most discoverable index for contributors. Tags add a layer of indirection without solving the underlying ambiguity.
References
docs-internal/architecture/v1-master-diff-merge.md§2 — full inventory and migration notes.docs-internal/architecture/v1-principles.md— principles P-2.x.llm-wiki/decisions/0004-component-delivery-model.md— establishes that components are CSS classes on standard HTML, which this ADR builds on.