Skip to content

A half-built design system made our page bigger than plain HTML

I measured one page in three versions: hand-written styled-components came to 32,149 characters, plain HTML to 23,139 and our design system to 18,040.

6 min read10 sections

Our marketing page exists in three implementations, and all three are on disk:

What it is
Aone HTML file with inline styles
Bthe production site: Next.js with hand-written styled-components
Cour own design system, only its components and tokens

Having three versions of one screen is unusual. It is also the only way to answer a question people argue about all the time and rarely measure: what does a design system save you?

Everything below was counted from the code of that same screen.

01

The hand-written version has the most code

Characters, not counting comments:

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

The design system version is 44% smaller than the hand-written one on the home page and 58% smaller on the contact page.

I did not expect B to be the biggest of the three, bigger even than the raw HTML.

The reason is obvious in hindsight and easy to miss in advance. Before B has a single page, it needs its own container, section, heading, buttons, stat strip and card: fifteen files that reproduce what a box and a text component already do. The plain HTML version never pays that cost, because it doesn't attempt a system.

So a half-built system costs more than no system at all, whichever library it is built on.

02

Design values written by hand

Code size is a weak metric. A count that says more about later work is how many design values are written into the code by hand.

home pagecontactper 100 lines
A521—198
B1973913
C1301

C has forty times fewer of them than the raw HTML and fifteen times fewer than the hand-written system.

The thirteen in C aren't scattered, either. They are three pixel values and ten flex-basis numbers, all in one file, and each has a written condition under which it goes away.

03

How many places one change touches

This is the comparison that someone who isn't an engineer can follow.

changeABC
brand color58 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 carries over

Changing the brand color means editing 58 places in the plain HTML version and one token in the design system, which then reaches all three platforms. I'd count that as a bigger saving than the characters.

04

B tokenized its colors and not its spacing

Look at the first two rows for B.

B tokenized its colors properly, with 48 custom variables and 276 usages. On color it is as good as the design system.

It did not tokenize its metrics, and it has 196 hand-written pixel values.

This is a common pattern, and I'd bet it's in your codebase too: teams tokenize what hurts right away and leave what will only hurt in a year.

A wrong brand color shows up in a screenshot, and someone complains the same day. Inconsistent spacing goes unnoticed for months and then becomes a redesign no one can find time for. So the values to tokenize early are the ones that don't hurt yet, like spacing.

In the design system, discipline doesn't come into it, because the choice isn't available: a padding prop will not accept a pixel value.

05

Two sources of truth that drift apart

B also shows what goes wrong when a system is adopted halfway.

B overrides 69 of the 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.

No one decided on that split. It is where the work stopped, and now half the surface tracks one source and half tracks the other, with nothing to say which is which.

06

What the compiler catches

I 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:

snippet.tsx
TypeScript · 10 lines
<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

The same mistakes, written as styled-components:

snippet.tsx
TypeScript · 12 lines
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 */`;

Here none of the ten is caught. All of it compiles.

The last two, paddin and colour, are accepted and do nothing, without an error or a warning. The element is not 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. The production site doesn't have that rule, so there the count is zero.

For someone new, that is the difference between an editor that won't let them get it wrong and finding out when someone notices.

07

What a developer has to remember

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 and seven widths. Font size can't be set at all. You pick a text variant instead of a number.

In B you have to memorize these values. In C the editor offers them as you type.

08

Documentation the build keeps current

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 or no fixture, or if the generated props are out of date. The documentation can't fall behind the code, because the build stops first.

I'd argue this is worth more than the savings in code. Components without documentation get re-implemented instead of reused, which is how you end up with twenty-six of them.

09

A second page built mostly from imports

The first page built on anything pays for the setup. So I 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 the time to fix one mistake.

I won't put that number next to human effort. It is an agent's time, and the baseline isn't clean either: the production site was also built with an agent, over two and a half days and thirty-eight commits, and Git measures calendar time, which is a different thing from work.

What I can say is where the speed came from:

first pagesecond page
navigation, footerinventedimported
gridinventedimported
tone rulesderived from contrastinherited
form—four components, assembled

Only the column ratio and the form were new. Everything else was imported.

The page chrome was extracted into its own file at that point, when a second page needed it, and not before. Extracting it earlier would have meant guessing at what the pages would share.

10

What I'm not claiming

  • The mobile figure in the table above only classifies the code. No port had been done at the time. The port has since been done and run on a device, and the post about porting this page describes it, along with the number it produced.
  • I have no measurement of build time that compares with human work, for the reasons above.
  • The contact form has no transport. It doesn't send anything anywhere, and both the code and the screen say so.
  • The three versions are not functionally identical. The original has dropdown menus and a burger menu, and the rebuild leaves them out by design.
  • The contact comparison needs a caveat. The production form has a captcha and a server action, which I didn't build in the new version. Even after generously removing sixty lines from the production side, it's 312 against 137.
Where this came fromMeasuring our marketing page in three implementations, one of them built with our cross-platform design system for Next.js, React Native and a Vite SPA.Read the case study →Related serviceDesign system and component libraryView service →Next postServerless WebSocket costs: connection minutes rarely dominateRead →

Writing

If you’re working on something similar, send us the details.

The first call takes 30 minutes, and you’re under no obligation.

Tell us about your projectcontact@microtech.consulting