Skip to content
Blog

We wrote the same page three times. The hand-written one was the largest.

Plain HTML, hand-written styled-components, and our own design system — measured on the same screen, and the result was not what we expected.

Design systems10 min read

We have the same marketing page in three implementations, all of them on disk:

What it is
Aone HTML file with inline styles
BNext.js with hand-written styled-components — this is the production site
Cour own design system, only its components and tokens

Three versions of one screen is an unusual thing to have. It’s also the only way to answer a question people argue about constantly and rarely measure: what does a design system actually save you?

Everything below is counted from the code, on the same screen.

The result we did not expect

Characters, comments excluded:

home pagecontact
A — plain HTML/CSS23,139
B — styled-components32,14911,293
C — design system18,0404,794

44% smaller than the hand-written version on the home page. 58% smaller on contact.

The part we had to sit with: B is the biggest of the three. Bigger than raw HTML.

The reason is obvious in hindsight and easy to miss in advance. Before B can write a page, it has to write a container, a section, a heading, buttons, a stat strip, a card — fifteen files reproducing what a box and a text component already do. The plain HTML version never pays that cost because it never pretends to have a system.

A half-built system is more expensive than no system at all. That’s the finding, and it has nothing to do with which library anyone chose.

The number that predicts pain later

Code size is a weak metric. This one isn’t: how many design values are written into the code by hand.

home pagecontactper 100 lines
A521198
B1973913
C1301

Forty times fewer than the raw HTML. Fifteen times fewer than the hand-written system.

And the thirteen aren’t scattered: three pixel values and ten flex-basis numbers, all in one file, each with a written condition under which it disappears.

What one change actually touches

This is the version of the argument a non-engineer understands.

changeABC
brand colour58 places1 variable1 token → 3 platforms
spacing scale373 pixel values196 pixel values0
add a light themerewrite the fileauthor 48 new variablesalready works
ship it to mobilenot possiblenot possible62% of the code goes

The pattern hiding in that table

Look at row one and row two together, for B.

B tokenized its colours properly — 48 custom variables, 276 usages. On colour it is genuinely equal to the design system.

B did not tokenize its metrics. 196 hand-written pixel values.

That is not ignorance. It’s a pattern, and we’d bet money it’s in your codebase too: you tokenize what hurts immediately, and you don’t tokenize what hurts in a year.

A wrong brand colour is visible in a screenshot and someone complains the same day. Inconsistent spacing is invisible for months and then becomes a redesign nobody can schedule.

The relevant difference isn’t that one team was more disciplined. It’s that in the design system the choice isn’t available: a padding prop physically will not accept a pixel value.

Two sources of truth, quietly diverging

There’s a second thing in B worth naming, because it’s the failure mode of adopting a system halfway.

B overrides 69 of 212 design system variables in its own theme file. The other 143 were never overridden — so they follow the system, while the 69 follow the local file.

Nobody decided that. It’s just where the work stopped. And it means half the surface tracks one source and half tracks another, with no marker anywhere saying which is which.

What the compiler catches

We wrote ten mistakes a new developer makes in their first week and ran them through both approaches.

In the design system, all ten are compile errors at the call site:

<Box padding="16px" />           // string is not assignable to a space token
<Box gap={7.5} />                // 7.5 is not a step on the scale
<Box background="teal" />        // not a background token
<Box maxWidth="1200px" />        // not one of the seven widths
<Box marginTop={-3} />           // negative steps are strings: "-3"
<Text variant="huge" />          // not a text variant
<Text tone="brand" />            // not a tone
<Text align="left" />            // it's "start", not "left"
<Box style={{ padding: 16 }} />  // there is no style prop
<Box border="default" />         // border is a boolean here

Ten out of ten.

The same mistakes, written as styled-components:

const A = styled.div`
  padding: 16px;
  gap: 15px;
  background: teal;
  max-width: 1200px;
  margin-top: -3px;
  font-size: 42px;
  color: #brand;
  text-align: left;
  paddin: 16px;     /* misspelled property */
  colour: red;      /* misspelled property */
`;

Zero out of ten. All of it compiles.

And look at the last two. paddin and colour are accepted and silently do nothing. No error, no warning, no clue — just an element that isn’t what its author thought it was.

A custom lint rule in our own repo catches three of the ten, and only where a token exists for the value. In the production site that rule doesn’t exist, so there it’s zero.

For someone new, that’s the difference between “the editor won’t let me get it wrong” and “you’ll find out when someone notices.”

What you have to keep in your head

has to rememberfrom where
B77 distinct pixel valuesthe code; written down nowhere
B43 theme variablesone CSS file
B30 distinct font-size declarationsscattered
B26 custom componentsundocumented
Cclosed setsautocomplete offers them

The design system’s sets are small and finite: nine spacing steps, six radii, fourteen text variants, eleven tones, twelve backgrounds, five shadows, seven widths. Font size isn’t available at all — you pick a variant, not a number.

The difference isn’t “less to learn.” It’s the difference between what you memorize and what the editor hands you.

Documentation that cannot go stale

design systemhand-written
registered components82
live fixtures63
static catalog pages810
a check that enforces ityesnone

The catalog check runs as part of verification and fails if a component has no entry, no fixture, or if the generated props are out of date. Documentation can’t fall behind the code, because the build stops.

That’s the part we’d argue is worth more than the code savings. Undocumented components aren’t reused; they’re re-implemented. Which is how you get twenty-six of them.

The second page

The first page of anything pays for the setup. So we built a second one — a contact page — to see what the system was worth once it existed.

It took four minutes and forty-nine seconds, from the first line to a green verification run, including a mistake we made and fixed.

We’re not going to put that number next to human effort, and we want to be explicit about why. It’s an agent’s time, and the baseline isn’t clean either — the production site was itself built with an agent, over two and a half days and thirty-eight commits. Git measures calendar time, not work.

What can be claimed is where the speed came from:

first pagesecond page
navigation, footerinventedimported
gridinventedimported
tone rulesderived from contrastinherited
formfour components, assembled

Only two things were new: a column ratio, and the form. Everything else was an import.

And the chrome was extracted into its own file at that point — when a second consumer appeared, not before. Extracting before the second consumer is guessing at what will be shared.

What we’re not claiming

  • None of this ran on a device or simulator. The mobile figure rests on which host components the code emits, not on a launched app.
  • No build-time measurement comparable to human work. See above.
  • The contact form has no transport. Nothing is sent anywhere, and it says so both in the code and on screen.
  • The three versions are not functionally identical. The original has dropdown menus and a burger; the rebuild deliberately doesn’t.
  • The contact comparison carries a caveat. Their form has a captcha and a server action we didn’t build. Even after generously removing sixty lines from their side, it’s 312 against 137.

What we’d take from this

Half a system costs more than none. The hand-written version is the largest of the three, and every line of that overhead went into rebuilding primitives that already existed somewhere.

Tokenize the things that don’t hurt yet. Colour gets tokenized everywhere, because a wrong colour is visible today. Spacing rarely does, because wrong spacing is invisible until it’s a redesign.

The real saving isn’t characters, it’s what one change touches. Fifty-eight places against one. That number is the whole argument, and it doesn’t need a chart.

This came out of building a cross-platform design system covering Next.js, React Native and a Vite SPA. Read the case study →

Related
The whole blog →

Half a design system?

We measure what your current setup costs per change, and tell you whether finishing the system is cheaper than keeping it. We reply within one business day.

Get in touch

Systems that can’t stop — from architecture to production.

© 2026 Micro Tech, Sarajevo