One component library, several products

A shared core repository across a hosting dashboard and the tools around it - what belongs in it, what does not, and the review habits that keep it from rotting.

A company with one product has a codebase. A company with four products has the same button implemented four times, slightly differently, and a designer quietly losing their mind.

That was the situation: a hosting dashboard, plus a set of tools around it - health checks, monitoring, a controller for the services behind a shop. Different applications, some Twig, some React, same company, same design language, four separate implementations of everything.

The fix is a core components repository. The fix is also where a lot of teams create a second problem.

Would every product need thisto change at the same time?yesnoCore libraryBUTTON · INPUT · TABLE · TOKENSStays in the productSERVER STATUS CARDthree products show server status — and answer different questions with it
Shared code has a cost, paid by whoever needs it to change next.

What actually goes in it

The test I settled on: would every product need this to change at the same time?

Buttons, inputs, selects, modals, tables, toasts, the spacing scale, the type scale, the colour tokens, the icon set. When the brand shifts, all of those shift together, everywhere. They belong in the core.

A “server status card” does not, even though three products display server status. They display it differently because they are answering different questions. Pulling it into the core means every future change needs to satisfy three products at once, and the component grows a prop for each disagreement. Six months later it takes eleven props and nobody can read it.

Shared code has a cost, and the cost is paid by whoever needs it to change. Only pay it for things that genuinely move together.

DashboardHealth checksMonitoringControllerTokens + CSS — defined onceCOLOUR · SPACING · TYPE · COMPONENT STYLESTwig markupReact markupmarkup per engine — because it has to be
The design decisions live once. Only the markup is written twice.

Two engines, one system

Part of the estate was Twig, part was React. The temptation is to build the library twice and keep them in sync by discipline. Discipline loses.

What survived was putting the layer that cannot drift in one place - tokens and CSS. Colours, spacing, type, and the component styles themselves live once, as stylesheets and custom properties. Twig templates and React components both consume them.

The markup gets implemented per engine, because it has to. The design decisions do not, because they should not. When a spacing value changes, it changes in one file and both stacks pick it up. When there is a rendering difference, it is a markup difference, which is visible, rather than a colour that is three shades off in one product, which is not.

One prop addedIN THE COREDashboardHealth checksMonitoringBilling flowyou cannot see the fourth one from where you are editing
The end-to-end suite is how you find out, before the billing flow does.

Tests are what let you change it

A shared library is only useful if you can change it. You can only change it if you are not frightened of what breaks.

Cypress end-to-end tests across the applications were what made that possible. Not because coverage is a virtue, but because a component in the core repository has consumers you cannot see from where you are editing. The tests are how you find out you broke the billing flow, before the billing flow finds out.

There is a real cost - the suite has to be maintained and it will be flaky sometimes. The alternative is a library everybody is afraid to touch, which becomes a library everybody copies out of, which is where you started.

Review is where the standard lives

Two habits kept it healthy.

Every change through a pull request, reviewed. Not ceremony - a component going into the core is an API for the whole company, and it lasts. The five minutes at review are much cheaper than the six months afterwards.

Refactor as you pass through. Restructuring is not a project you get funded; it is something you do to the file you already have open. A codebase stays readable through hundreds of small corrections, never through one big cleanup that gets deprioritised in March.

Components into an application you are not replacing

The harder version of this is putting new components into an application that already exists and is not going anywhere.

On a wealth administration platform, the estate was Ember, mature and load-bearing. New work was React. The realistic answer is not a rewrite - it is building generic React components and mounting them inside the Ember application, screen by screen, while the rest of the system carries on.

What that teaches you is where a component boundary actually is. A component that reaches for global state, router context, or a shared store is not portable, and you find out immediately because none of that exists on the other side of the boundary. Props in, events out, data fetched through explicit REST calls - constraints imposed by the integration, and better components because of them.

The library work and the integration work turned out to be the same skill. Both are about deciding what a component is allowed to know.

The measure

Not how many components are in it. How often someone building a new screen reaches for the library first - and how rarely they need to fork something out of it because it did not quite fit.

If people are copying components out, the library is not too small. It is answering the wrong question.