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.
The four clauses
Section titled “The four clauses”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.
A component API is an API
Section titled “A component API is an API”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.
Stories are fixtures
Section titled “Stories are fixtures”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.
The escape hatch problem
Section titled “The escape hatch problem”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:
- 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.
- 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.
- 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.
Composing versus extending
Section titled “Composing versus extending”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.
Status means the contract, not the code
Section titled “Status means the contract, not the code”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.
Where it connects
Section titled “Where it connects”- Design system is what this contract implements and enforces.
- Storybook is the tool the stories run in.
- Interaction tests and accessibility tests are what makes the contract checkable.
- Keeping them aligned is the drift problem between this catalogue and the design one.