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:

  1. 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.
  2. 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.
  3. Is it assembled from two or more atoms with no such orchestration? It's a molecule — Card, Alert, FormField, Pagination.
  4. 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

  • SegmentedControl manages a selected value and arrow-key navigation, but across sibling radio options with no portal or open/closed state — molecule.
  • Accordion manages expanded state across multiple items — organism, even though it renders no portal.
  • NavigationMenu renders an always-visible nested list — every item with an href is 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.
  • FormField wires ids and ARIA attributes to an arbitrary child — wiring, not orchestration — molecule.
  • VerdictBadge and DecisionCard were 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.
  • DecisionCard shipped in organisms/ and moved to molecules/ 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 in packages/react/scripts/tier-boundary.test.mjs now enforces that: every component in organisms/ must orchestrate, or compose something that does.
  • StreamingCaret is 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 (DecisionCard composes VerdictBadge); atoms compose nothing from the system.