Skip to content

Component library

If the design system decides what the interface is made of, the core component library is the interface that user-interface code is actually written against.

That is the sense in which it drives coding: a screen is assembled from things already under contract, so most screen-level decisions stop being decisions.

Core is doing work in that name. The library is the shared, versioned set that every product builds from — not everything that appears on every screen. What a feature team composes on top of it is theirs and changes freely; what is in the core changes by change request, with deprecations. Knowing which side of that line a piece of work sits on is most of what makes an estimate honest.

The live catalogue is on the portal: component library.

A component contract has four parts. A component that answers all four can be depended on without reading its source, which is the only definition of a component library that means anything.

Props. Names, types, defaults — and what is deliberately not exposed. The last part is the one that decays: an escape hatch added quietly becomes the contract everybody depends on within a month.

States. Rest, hover, focus-visible, active, disabled, loading, error, empty, read-only. One story per state, so a missing state is a missing artefact rather than an unstated assumption.

Accessible behaviour. The role it exposes, the name it takes, the keyboard model it implements, what it announces when it changes. These are guarantees, not options, and they are the reason a library is worth more than a folder of snippets.

Tokens consumed. Which design-system roles it reads. This is the seam where the visual contract enters the technical one, and it is the only place in the codebase where a colour may be decided.

The same discipline applies, and the same mistakes are available.

API design rule In a component
Name for the caller’s intent, not your implementation tone="danger", not red={true}
Defaults should be the safe common case A button is type="button" unless told otherwise
Do not expose what you cannot keep No className passthrough into internals
Additive changes are cheap, removals are not A new optional prop is free; a renamed one is a migration
The contract is the docs The stories are the specification, not an illustration of it

The last row is the one worth dwelling on.

A story is a component in a named, reproducible state. That single property is what turns a documentation tool into a testing one:

  • Interaction tests run against a story, exercising the component in a known state without assembling a page around it.
  • Accessibility scans run per story, so a violation is attributed to a component rather than to a screen.
  • Visual regression compares renders of the same stories across commits, catching the class of change no assertion is written for.
  • The stories are the review surface — a designer reviews the real rendered component in every state, not a screenshot of the happy path.

It changes development too, in the same way a mocked API does. Building a component against its stories means building it against its declared states before it is wired into a page or connected to data. Both remove a dependency on something that is not ready, and both do it by working against the contract.

Changing a component without breaking six screens

Section titled “Changing a component without breaking six screens”

The problem every library hits in year two.

Additive first. A new optional prop with a safe default breaks nothing. Most requests can be met this way if you are willing to think for ten minutes first.

Deprecate, do not remove. Mark it, warn in development, keep it for a stated period. A removal that arrives without a warning teaches consumers to pin the version, and a pinned consumer is outside the contract entirely.

Codemod what you can. A rename that ships with a codemod is a chore; a rename that does not is a negotiation with six teams.

Version the contract, not the implementation. An internal rewrite that preserves all four clauses is not a breaking change, however large the diff. A one-line change to a default is one.

A consumer needs something the contract forbids. This is not a rare edge case; it is a weekly occurrence in a healthy library, and how it is handled decides whether the library survives.

The bad answers: refuse, and they fork the component; allow a style passthrough, and the contract is over.

The workable answer is to treat it as a contract change request, and to ask which of three things it is:

  1. A missing variant. The need is general. Add it to the design system, then to the component. This is the good case and it makes the library better.
  2. A genuinely local exception. One screen, with a reason. Allow it outside the component — composition, a wrapper, a documented slot — so the exception is visible in the screen rather than hidden in the library.
  3. A design mistake upstream. The screen needs it because something earlier went wrong. Send it back to critique.

What must not happen is the fourth outcome: a quiet prop added to satisfy one caller, undocumented, which becomes load-bearing for four more.

The everyday decision, and the one worth being explicit about in a four amigos session.

Composing Extending the core
What it is Assembling existing components into this feature’s screens Adding to, or changing, the shared library
Who decides The feature team The library owners, on a change request
Blast radius This product Every product
Speed Hours Days, and rightly so
Reviewed against The journey The contract, plus every existing consumer

The healthy ratio is heavily weighted towards the first column, and it gets more so as the library matures. A team that finds itself extending the core weekly either has an immature library or is solving product-specific problems in the wrong layer — and those two have opposite remedies, so it is worth knowing which.

On the portal catalogue each component carries a status, and it describes the stability of the contract a consumer is depending on:

  • stable — props, states and accessibility are agreed and under test; this will not change without a deprecation
  • beta — it renders and is usable, but the contract is still moving under you
  • planned — the need is agreed, nothing exists

A polished-looking beta component is still a beta contract. Consumers care about the second thing, and conflating them is how a library acquires users it cannot afford to break.