Composition patterns

The house rules for composing components: composition fidelity, slots for consumer artwork, the single-container flip and RTL.

2026-11-07

Composition fidelity

The house ruling: composition fidelity is the API. If component A composes components B, C and D in Figma, the React component MUST import and compose the real B, C and D. Never re-implement a component that already exists in a package.

If a composed child does not exist in code yet, it is a build gap — build it as its own standalone component first, then import it into A. A missing child is never a licence for placeholder markup or an inline HTML/CSS imitation.

Two failures this rule exists to prevent, both learned the hard way:

  • A "Countdown" drawn inside a header turned out to be a remote-library CARD, not the bare ghost-core countdown timer. Rendering the timer in its place shipped the wrong background — the fix was building the card as a component, not styling the well.
  • A composed child can be simply absent from code. Composition audits must check for children with NO code equivalent, not only for re-implementations.

Slots are for consumer-owned artwork

A ReactNode slot is legitimate ONLY for consumer-owned content — crests, sponsor logos, background imagery, broadcaster marks — or where the composed child's data is genuinely consumer content. When a slot stands in for a real component, the STORY mock must pass the real component, never an inline imitation; and prefer promoting the composition into the component itself when the child's props are expressible as data.

A composition resolves the colour way once

A parent's colour way does not imply its children's, and children rarely support the same subset of the ramp. A composing component resolves the way to its own subset at the boundary and passes the resolved value down — it does not forward its raw colourSet prop to several children that would each degrade it independently. A page template goes further and PINS each child's colour set and heading level, because only the page can see the whole outline. Each component's figma.mdx / theme.mdx in Storybook records the mapping it uses.

Common patterns

A few structural patterns recur across the estate. This is the high-level shape of each — the per-component specifics, and the tests that guard them, live in Storybook and in .claude/rules/frontend/.

Responsive single-container flip

When a component switches from a column layout on mobile to a row layout on desktop, exactly ONE container owns the switch. The root stays flexDirection: 'column' full-width; one inner container flips to row at mediaUp('lg'). Styles are written mobile-first — the base comes from the Figma Breakpoint=S design, and mediaUp() overrides come from Breakpoint=XL. Breakpoints are the Ghost set (360 / 768 md / 1024 lg / 1440 xl / 1920 xxl), always via the mediaUp() helper, never hardcoded, and never container queries for layout.

Two authored rungs leave two bands no design describes: the tablet band (768–1023) and the laptop band (1024–1439). On this site a band's outermost container binds the page measure rather than the X-Large container, and components that lay out columns or rails take a declared tablet step at md. The full ruling is on Responsive rungs.

A responsive decision that belongs to one consumer is expressed as a DECLARED prop mode (lgOrientation, lgSize, lgRootWidth) defaulted to preserve today's behaviour — not as a media rule baked into a shared primitive, which would silently break its other consumers.

Directional icons and RTL

Everything uses CSS logical properties from day one — marginInlineStart not marginLeft, insetInlineStart not left — so the LAYOUT mirrors under dir="rtl" for free. Logical properties do NOT mirror the ARTWORK, though: an arrow drawn pointing right keeps pointing right on an RTL page.

So directional glyphs opt in, per icon slot, to the single house mechanism — the directionalIcon style plus data-directional="true" — which mirrors only inline-axis navigation glyphs (IconChevronRight, IconArrowLeft used to mean "next"/"back"). It is never applied blanket: block-axis and semantic glyphs (disclosure chevrons, play triangles, logos, ticks) must not flip. The decision is declared in the DOM, and guarded by unit and e2e tests.

Motion

What moves, and with which values, is decided by ELB's design-owned motion spec, not by the component author: every animation names its spec pattern (P-REVEAL, P-CASCADE …), takes its values from the global motion.* tokens, and under reduced motion renders the complete final state at once. The mechanics are the createTransition() / createAnimation() helpers, which fold in the prefers-reduced-motion: reduce override automatically; only transform and opacity are animated. See Motion.

Each component's accessibility.mdx in Storybook documents its motion, focus and keyboard behaviour; the .claude/rules/frontend/ rules carry the full engineering detail.