The tier boundary
Components live under packages/react/src/components/<tier>/ — atoms, molecules,
organisms, or ai. The boundary is a rule you can apply mechanically, not a judgment call, and it
decides where a component lives and what it's allowed to do.
The rule
Ask these questions in order:
- Does it have any meaning outside an assistant surface? If not, it's ai — ChatThread, ChatMessage, StreamingCaret. This question comes first because it's the only one about the domain rather than the shape, and it overrides the rest.
- Does it render into a portal, trap focus, or manage open/closed state across multiple sub-elements? It's an organism — Dialog, Select, Combobox, DropdownMenu.
- Is it assembled from two or more atoms with no such orchestration? It's a molecule — Card, Alert, FormField, Pagination.
- Otherwise it's an atom — single-purpose, not further divisible: Button, Input, Badge, Separator.
Why the line sits there
The boundary tracks behavioral complexity, not visual size. Overlay orchestration — portals, focus management, keyboard navigation across children — is where accessibility bugs live, so the components that do it are quarantined in one tier where the shared interaction hooks and heavier test suites apply. A molecule stays simple enough to review at a glance; the moment it grows open/closed state across sub-elements, it's an organism and moves.
ai is the one exception, and it's deliberate: it tracks domain, not complexity.
StreamingCaret is atom-shaped and ChatThread is molecule-shaped, and filing them by
shape would have hidden the thing a reader needs to know — that they encode assumptions
about generated, incremental, non-deterministic content. The test is whether you can
describe the component without saying "assistant," "model," or "streaming." If you can,
it isn't ai. Being a new tier buys a directory and nothing else: the same ds- classes,
token rules, forwardRef requirement, touch targets and contrast contract apply.
Edge cases, decided
SegmentedControlmanages a selected value and arrow-key navigation, but across sibling radio options with no portal or open/closed state — molecule.Accordionmanages expanded state across multiple items — organism, even though it renders no portal.NavigationMenurenders an always-visible nested list — every item with anhrefis a real link (an item without one is an inert group label), no disclosure state. It's an organism for its compound structure, but note it deliberately has no open/close behavior.FormFieldwires ids and ARIA attributes to an arbitrary child — wiring, not orchestration — molecule.VerdictBadgeandDecisionCardwere designed for an assistant product and are still molecules. A verdict marker a model happened to produce is a verdict marker; a card that happens to render generated figures is a card. The domain question is about the component, not about who supplied its props.DecisionCardshipped inorganisms/and moved tomolecules/in v3. It renders no portal, traps no focus, and holds no open/closed state — it has no hooks at all — so the rule above makes it a molecule and the weight of the surface does not enter into it. A sweep inpackages/react/scripts/tier-boundary.test.mjsnow enforces that: every component inorganisms/must orchestrate, or compose something that does.StreamingCaretis a single<span>with no children — atom-shaped by the rule above, and ai by question 0.
What the tier changes in practice
- The import path:
@elirobinson/react/components/<tier>/<Name>— moving a component between tiers is a breaking change. - Review depth: organisms require keyboard-contract tests; their component pages must document focus behavior precisely.
- Composition direction: organisms may compose molecules and atoms; molecules compose
atoms and other molecules (
DecisionCardcomposesVerdictBadge); atoms compose nothing from the system.