How composition fails

Eight classes of failure, each of them invisible while components are inspected one at a time and each appearing only on assembly. This is the core of the argument, and the list is drawn from observed behaviour rather than from theory.

Semantic collision

The component’s semantics are correct alone and wrong, invalid or ambiguous in context.

  • A card that owns an h3 produces a broken outline when placed directly under an h1.
  • A navigation component nested inside another nav yields two indistinguishable landmarks unless each is named.
  • A page-shell component and a content component both emit main.
  • A button nested inside a clickable card creates nested interactive controls, with ambiguous activation and an unclear accessibility tree.
  • A list component wrapped in a layout element that applies role="presentation", or in a grid container that breaks the ul/li parent-child relationship, silently loses list semantics.
  • A cell component is valid only inside the expected row and table ancestry. Used standalone its role is meaningless.
  • ARIA composite widgets impose required owned roles and ordering, so a slot that accepts arbitrary children can break a required parent-child role relationship.

The APG’s governing rule applies at exactly this boundary: no ARIA is better than bad ARIA. A composition that produces incorrect ARIA can make the non-visual experience worse than the same page with no ARIA at all, which means composition is not merely a missed opportunity to improve things. It can actively destroy what the primitives were giving away for free.

Accessible name collision

Names are computed from content and from relationships that cross the component boundary, so assembly can corrupt a name that was correct in the demo.

  • Duplicate landmark or region names that were unique in isolation.
  • An aria-label on a wrapper overriding a meaningful visible label beneath it.
  • Twenty repetitions of “Read more” or “Edit” with nothing to distinguish them, each one individually passing a name check.
  • aria-labelledby and aria-describedby broken by identifier collision when a component is instanced more than once on a page, or by identifier regeneration on re-render.
  • A visible label and an accessible name that diverge, which breaks speech input users who say what they can see, as well as confusing screen reader users who hear one thing and read another.

The last of those is the one most often treated as cosmetic. It is not. If the name and the label disagree, a speech input user has no way to address the control at all.

Focus competition

Each component manages focus correctly. Together they fight over it.

  • A dialog traps focus while a combobox inside it also manages focus and also wants Escape.
  • A route change moves focus to the new h1 while a toast simultaneously claims it.
  • A disclosure collapses while focus is inside it, dropping focus to body.
  • A sticky header visually covers the element that has just received focus, so the focus indicator exists and is not visible.
  • Two roving-tabindex groups nested one inside the other share arrow-key handling.
  • A virtualised list destroys the focused row on scroll or on refetch.
  • An asynchronous update re-renders the subtree containing the focused element, and focus is lost.
  • Focus restoration on dialog close targets an element that the underlying page has since removed.

The conclusion is structural. Focus is not solely a component property. It requires arbitration by an owner at the containing level, and if no component has been appointed to that job then the behaviour is whatever the event order happens to produce on the day.

Keyboard grammar conflict

Keys are contextual, and nesting creates ambiguity that neither component can resolve alone.

  • Arrow keys move between tabs, move within a menu, move a grid cell, adjust a slider, or scroll the page. Which wins when a grid sits in a tabpanel inside a dialog?
  • Escape closes a popover, cancels inline editing, closes the dialog, or exits an application mode. A single press must resolve to exactly one of those.
  • Enter submits the form, or selects the highlighted combobox option.
  • Space activates the button, scrolls the page, or toggles the checkbox.
  • Typeahead in a listbox competes with application-level or browser-level single-key shortcuts.
  • Tab escapes the composite, or moves within it.

A design system cannot merely assert that each child is keyboard operable. It has to state how the combined grammar resolves and which component is the resolver. “Both components are keyboard accessible” is true and useless in the case where they both want the same key.

Announcement collision

Independently reasonable live regions become unusable together.

  • Validation errors, autosave status and result counts all announce at once.
  • A route-change announcement duplicates the announcement of the new page heading.
  • “Loading” is immediately superseded by “42 results”, producing either a stutter or a swallowed message depending on the screen reader.
  • A polite region is starved because an assertive region keeps interrupting.
  • Rapid filtering fires an announcement per keystroke.

Assistive technology behaviour for live regions has historically varied sharply between browser and screen reader combinations, which has a direct consequence for how this is recorded. A design system must hold evidence per combination here, not a single boolean. “The live region works” is not a statement that can be true across the matrix.

Layout and reflow interaction

Components compete for finite space, and the competition produces failures that neither component causes alone.

  • Two independently responsive components impose incompatible minimum widths, producing horizontal scrolling at 320 CSS pixels.
  • Increased text spacing or 200% zoom clips text in a component that was sized for its demo string.
  • A sticky region occludes focused content, or occludes the target of an in-page link.
  • Touch targets that meet the minimum size individually overlap, or fall below spacing requirements, when packed at narrow widths.
  • Tooltips and popovers are clipped by an ancestor’s overflow: hidden, introduced by a layout component that knows nothing about them.
  • CSS grid or flex order makes visual order diverge from DOM order and therefore from focus order.
  • Two nested container queries produce an unusable intermediate state that neither component’s own breakpoints anticipated.

This is where the intrinsic layout method is load-bearing rather than a stylistic preference. Components that declare their intrinsic requirements and adapt to the space actually available compose far more predictably than components that assume a viewport, because a viewport assumption is a claim about the world and the world includes a user at 400% zoom.

Content-dependent failure

Component demonstrations use short, curated, well-formed content. Real assembly does not.

  • Long strings, long unbroken words, and translations that expand by thirty to forty per cent.
  • Repeated headings and repeated controls.
  • Missing images, missing alternatives, missing descriptions.
  • Error, empty, loading, partial, stale, offline and permission-denied states.
  • User-generated content carrying its own headings and markup.
  • Density: five rows against five thousand.
  • Meaning carried by position or colour that the component cannot express semantically.

This forces the distinction a component contract most needs: structural guarantees against author obligations. A card can guarantee the association between its label and its control. It cannot guarantee that the author supplied a meaningful heading. Both facts belong in the contract, and the second is the one usually left out, which is why GOV.UK requires components to be tested inside realistic pages with representative content rather than in isolated demonstrations.

Override and escape-hatch invalidation

Every styled library provides escape hatches: className, style, as or asChild, slots, render props, portals, raw HTML injection. Each one can silently invalidate a guarantee.

  • Restyling a focus indicator below the non-text contrast threshold.
  • Polymorphic as="div" on a component whose keyboard behaviour assumed a button.
  • A slot accepting a child that breaks a required owned-role relationship.
  • A portal moving DOM out of the reading order it was tested in.
  • CSS display: contents removing implicit semantics from a wrapper in some engines.

The consequence for a design system is that overrides have to be treated as guarantee-invalidating events which trigger retest, not as neutral styling. An escape hatch is not a styling API. It is a hole in the contract, and it should be recorded as one.

References