Back to Blog
Design Engineering · Essay

Your design system is adopted. It still isn't used.

Simon Doba·August 18, 2026·7 min read

The usual way to measure a design system is adoption: how many components exist, how many are imported, what percentage of the app is on the system. Every number goes up over time and none of them tells you anything.

I ran a different measurement against one of my own applications — 389 files, 104 routes, 10,688 call sites across 39 components, and the number that turned out to matter was not which components get used. It was how many different ways one component gets called.

Spread

Take every call site of a component, reduce each to its prop signature, count the distinct ones, divide by the total. Two examples from that run:

  • input: 411 call sites, 357 distinct signatures. 87%.
  • card: 3,162 call sites, 240 signatures. 8%.

Those two components live in the same system, in the same app, maintained by the same person. One is being used as a system: a small set of ways to do the thing, repeated. The other is being reinvented at almost every call site, which means the component is a container for decisions rather than a decision that was made once.

A high spread is not automatically bad. Some components genuinely take a wide range of inputs. But it is the number that tells you where the system stopped deciding things and started deferring them, and no adoption percentage will ever surface that.

The finding I did not go looking for

Button variants, by call site:

  • outline — 512
  • ghost — 239
  • default5

The primary action is essentially never assigned. Across an entire application, five screens make a claim about what the user should do next, and everything else is presented as equally weighted.

That is not a component problem. Nothing is broken, no lint fires, every one of those buttons is a legal use of the system. It is a product problem that only became visible because the variants were counted rather than reviewed. I have used this app for months and never noticed, because on any single screen a row of outline buttons looks deliberate.

The rest of that pass was more ordinary and still useful: 26 icon buttons with no accessible name, 388 call sites with raw colour values, 94 with arbitrary utilities. Those are the ones a linter could have caught if I had written the lint, which is the argument for machine-readable rules in the first place.

The dimension that was missing entirely

The first version of this profile only counted components from my own system. It looked complete. It was measuring an app I had already decided the shape of.

Adding third-party libraries to the scan turned up 54 of them, 45 with no equivalent in the system at all. One of those is doing real work: every metric on every dashboard animates through NumberFlow, and a profile that counted only my own primitives could not see it. As far as that version was concerned, those screens were made of cards and text.

So the profile was not just incomplete, it was confidently incomplete — the same failure I keep running into elsewhere. What gets measured is decided before anything is measured, and the instrument cannot report on what it was not pointed at.

What this is and is not

Being clear about the size of the claim, because the numbers are specific enough to look like more than they are:

  • One codebase. Everything above is a self-diagnosis of an application I built. It says nothing about anyone else’s ratios, and I would not publish a benchmark from it.
  • I am both the measurer and the measured. The system, the app and the analysis are all mine, which is exactly the position where a flattering result should be distrusted. The button finding survives that test mostly because it is unflattering.
  • The method is the transferable part. Distinct signatures over call sites is four lines of analysis on top of a TypeScript AST walk. The ratios are mine; the number is anyone’s.

One implementation note, because I got it wrong first: the analysis originally ran on regular expressions and reported 132 never-imported components. After fixing a greedy match and missing sibling imports, the real number was 56. More than half of a finding was my own parser. It runs on the TypeScript compiler now, which is slower and does not invent things.

Next

A second codebase, and ideally one I did not write, because the interesting question is whether spread separates systems from libraries in general or only describes how I build. Until then this is a method with one example attached.

I also want the profile to run in CI and fail on a rising spread, the way a bundle budget fails on a rising size. That needs a baseline, and a baseline needs more than one measurement.

If you maintain a design system: do you know how many distinct ways your most-used component gets called? I would guess most teams have the adoption number and not that one, and I would be curious to be wrong.

Measured 5 August 2026 against one Next.js application: 389 files, 104 routes, 10,688 call sites across 39 components. Analysis runs on the TypeScript compiler rather than regular expressions, for reasons described above.

Share this article

Building something similar?

I write about setups I actually use. If you're working on something comparable, I'd be curious what your workflow looks like.

Get in touch

Cookie Settings

We use cookies for analytics and to improve our website. Privacy policy