Serhii.Get in touch
Back to work
SaaS · SolidWay2024 — PresentReact Developer · Library architect

A component library three squads adopted

Three product squads were rebuilding the same Button, Modal, Input, and Table over and over. I architected an internal library with token-driven theming, per-component tree-shaking, and Storybook + Chromatic visual regression. All three squads adopted in two quarters.

−30%
UI duplication
+40%
Time-to-Interactive
3
squads adopted (in 2 quarters)
React 18TypeScriptViteStorybookChromatic

The problem

At SolidWay, three product squads were building related-but-distinct SaaS surfaces on the same React stack. Each had its own UI primitives: three Button components, three Modal implementations, three different Input APIs. Design QA was burning ~30% of cycle time chasing inconsistencies between squads.

The team had tried "everyone use the same npm package" once and it failed in three months because:

  • The package shipped one giant bundle (no tree-shaking)
  • Theming was hard-coded; squads needed slightly different brands
  • No visual regression — breaking changes shipped silently
  • API surface was opinionated in ways that didn't match new use cases

So when I proposed taking another swing at it, scepticism was earned.

The architectural decisions that mattered

1. Tokens are CSS custom properties, not JS

Every component reads from CSS variables — never from a JS theme object. --color-primary, --space-3, --radius-md. No ThemeProvider wrapper, no runtime cost, and squads can override per-route by setting variables on a parent element.

This sounds boring but it's the single biggest unlock: dark mode is free, brand variants are a 4-line CSS override, and the bundle has zero theme-related JS code.

2. Per-component imports as the public API

Instead of import {Button} from '@app/ui', the public API is import {Button} from '@app/ui/Button'. Each component is published as its own subpath export with its own CSS file. Tree-shaking just works, and the bundle cost of the library starts at ~2KB per component.

3. Storybook stories are the visual regression suite

Every story in Storybook is auto-snapshotted by Chromatic on each PR. Visual diffs block the merge until reviewed. This eliminated the "something tiny shifted, nobody noticed for two weeks" class of bug that killed library v1.

Note
We don't require 100% visual regression coverage — just stories for primary states, sizes, and dark mode. About 70% coverage catches 99% of accidental regressions in our experience.

4. Adoption strategy: opt-in, not mandated

I didn't announce "all squads must use this". Instead I picked the squad with the worst duplication pain (Settings) and shipped a migration with them as the pilot. Two months later, the other two squads asked to migrate themselves — because the Settings team was shipping faster.

Mandates create resistance. Pilots create demand. The library got adopted because squads wanted it, not because they had to.

The component design rules I enforced

  1. No business logic. A <DataTable> component doesn't fetch data. It accepts rows. Period.
  2. One required prop per component, max. Everything else has sensible defaults. <Button>{children}</Button> works.
  3. Forward refs by default. Every interactive primitive uses forwardRef. Required for focus management, measurement, and scroll APIs.
  4. Keyboard parity with mouse. Every interactive surface tested with keyboard-only navigation in CI.
  5. Dark mode = same code, different vars. No isDark branches in components.

The result

  • UI duplication: dropped 30% measured by AST-similarity scan across the 3 squads' codebases
  • Time-to-Interactive: +40% on the heaviest squad, partly from per-component tree-shaking removing ~120KB of unused UI code
  • Adoption: 3/3 squads in 2 quarters, voluntary migration after the Settings pilot
  • Design QA: cycle time dropped from ~30% of sprint time to ~10%

What I'd do differently

I'd publish the library to a private npm registry from week one. We started with monorepo workspaces, which worked but made it impossible to A/B-test versions across squads. A real semver flow with changesets would have unlocked "Settings is on 2.4, Accounts is still on 2.2" — useful when one squad needed to delay a breaking change.

I'd also write a public migration guide before the pilot, not after. The Settings team was the migration guide; the next two squads had to reverse-engineer it. Six hours of writing would have saved each of them a day.

Next case study

RN cold-start −25% on a HealthTech app

HealthTech · TenThousand · 2022 — 2023