Composition patterns
The house rules for composing components: composition fidelity, slots for consumer artwork, the single-container flip and RTL.
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
countdowntimer. 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.