Skip to content
Blog

Seven components and four exceptions

We rebuilt our marketing page in our own design system — not to redesign it, but to find out exactly where the system runs out.

Design systems9 min read

We have a cross-platform design system covering Next.js, a Vite SPA and React Native. It has 82 registered components, a catalog of 81 pages, and a verification layer that fails the build when a convention is broken.

None of that tells you whether it can carry a page.

A component library tests itself component by component, which is a test of the parts and not of the whole. So we took the existing marketing page — one HTML file with inline styles and a small reactive script — and rebuilt it entirely from the system.

The goal was never a nicer page. The goal was a list: which components carry a real screen, which four things are missing, and what each missing thing actually costs.

Seven components carried the page

139 elements in the finished page come from the design system:

Box       63
Text      55
Link       9
Divider    7
Icon       2
Badge      2
Surface    1

Seven components for an entire marketing page. Not the eighty-two in the registry — seven.

That was the first useful finding, and it inverts the assumption a library is usually built on. The catalog wasn’t too narrow. Breadth wasn’t the constraint at all.

Four exceptions, each with a name and a delete condition

Against those 139 elements there are exactly four escapes from the system, all collected in a single file rather than scattered:

  • three <div style> — flex-basis for grid cells, and position: sticky
  • one styled.a — the skip link

Each one has a written reason and a condition under which it disappears. That’s the difference between an exception and a leak: an exception you can count, explain, and eventually remove. A leak just spreads.

The ledger — eight things the system couldn’t do

This is the actual output of the exercise. Not the page — the list.

WantedWhy notWhat the page got instead
48px headingdisplay is 36px; the 5xl step exists in the scale but no variant consumes itsmaller heading
A link that looks like a buttonButton has no to; the accent background token isn’t full-strength; the base recipe fixes the radiuswhite pill instead of the accent colour
Italicfont-style doesn’t exist anywhere in the packagesconstraint quotes without italics
Responsive gridBox has no basis or min-width, and min-width: 0 is hardcodeda <div style> per cell
position: stickyYoga has no sticky, so the position type doesn’t include ita <div style> around the bar
Skip linkthe visually-hidden component takes a text prop, with no children and no focus stateone styled.a
Dashed or single-sided borderthe border prop is a boolean, one colour, all four sidesa tinted pill instead of a dashed outline
Landmark rolesthe role type has no main or contentinfo, because React Native doesn’traw <main> and <footer>

Two of those were already sitting in our plan as future work — a hero font size and a link-styled button. Building the page turned them from an assumption into a measured finding with a cost attached.

That’s the argument for doing this at all. A backlog assembled in a meeting is a list of things someone imagined wanting. A backlog assembled by building a real screen is a list of things that actually stopped you, in order, with evidence.

How much of it goes to mobile

This is where we have to be careful, because there are two honest numbers and one dishonest one.

Across everything shipped — 1,402 lines — 76% could run on React Native. That number is inflated, and we’re not going to use it. 521 of those lines are a content file: pure text in two languages, no imports, no JSX. Of course it ports. Counting it is padding.

The honest cut is the UI code alone — 881 lines:

linesshare
Portable as-is30635%
Portable after substitution24127%
Web-only33438%
Usable on mobile54762%

“After substitution” means the grid wrappers get replaced by a plain Box. On a phone the layout is a single column anyway, so that substitution is mostly deletion, not rewriting.

And the web-only third deserves a second look. 70 of those 334 lines are blocked by a single <a href> — outbound links and mail. That’s a swap for the platform’s link-opening call, not an architectural boundary.

The genuinely irreducible web-only code is about 155 lines, roughly 18%: the sticky navigation, the footer, the main landmark with its skip link, and the framework’s routing functions.

All of which is shell. None of it is content.

By section, out of ten blocks on the page: two are portable as-is, four after substitution, four are web-only — and all four of those are navigation, about, footer and the route wrapper.

Two things came out better than the original, for free

Neither was an objective.

It works in both themes. No colour is written anywhere in the page; everything resolves through semantic tokens. The original hardcodes its background and accent, so it exists only in dark.

Both languages are indexable. Language is a route segment rather than a stored preference, so each language prerenders as static HTML — about 152 KB per language. On the original, a crawler only ever saw English.

The whole page is a server component. No client islands, no state.

Three silences

Here’s the part we’d take to any team, regardless of whether they have a design system.

Every one of the three real problems we found had passed the compiler, the linter and the build.

An icon name that was wrong and broke nothing

We asked for a bullet point. The name we used turned out to be an alias for a list icon — three lines with dots, not one dot.

The icon registry is open by design, so names are typed as strings. Which means: the compiler is quiet, the linter is quiet, the build passes, and the render produces the wrong glyph — or an empty box of the correct size.

It was caught by looking at a screenshot. Nothing else in the chain had an opinion.

The fix, incidentally, was to stop using an icon: a badge with no children is an 8px dot, and it was also the only way to get the full-strength accent background the ledger says we’re missing.

The lesson is about the trade. An open registry buys extensibility and pays for it in silent misses. Our verification layer checks the 63 live catalog fixtures, not application code — so it could never have caught this.

A native test that stays green for a screen that cannot open

This one is worse.

Render the web-only wrappers under the native test project and the test passes, printing this quite calmly:

{"type":"div","props":{"style":{"position":"sticky","top":0,"zIndex":100}}}
{"type":"a","props":{"href":"#x"}}

The test renderer records a tree. It does not ask whether a host component exists. On an actual device, a div fails at the view config lookup.

And the import doesn’t save you either: the native styled-components proxy returns a function for styled.a as readily as for anything else, because it accepts any key.

Four levels of silence: import, compiler, linter, test.

The check that does work is small — enumerate the host components in the rendered output and fail if anything other than View or Text appears. That’s also how we proved the one genuinely portable card actually is portable: it emits only View and Text, and nothing else.

A decorative dot inside an accessible name

The brand dot at the end of the heading was being read as part of the accessible name, with a space in front of it: “…systems that can’t stop .”

Small. But it’s the same category as the other two: the accessible name we assumed and the accessible name we measured were different strings, and only one of them was real.

What a mobile port would actually take

Not a rewrite. A different shell.

WebMobile
grid wrappersgone — one column, a box with a gap
sticky baran app bar, or nothing
skip linkmeaningless — no tab focus
<main> / <footer>a screen wrapper: safe area, scroll, content
framework routingthe navigation adapter that already exists
<a href> for mail and outbound linksthe platform link call

The practical move is to extract each section into a file that takes only design-system props, and let each platform bring its own shell. The one card that ported without a single change is already shaped that way — which is why it ported.

What we did not measure

We’d rather say this than have someone assume it.

The page has not been run on a device or a simulator. The portability claim rests on which host components the code emits, not on a launched application. “It emits only React Native host components” is true. “It’s been tested on a phone” is not, and we’re not going to write it.

No Lighthouse run, no client bundle measurement. The verification suite passes — 674 tests across four projects, exit code zero — and in a headless browser there were no console errors, no horizontal scroll at 390px, 29 links with correct accessible names, and a clean heading hierarchy. That’s what was checked. Performance was not.

The mobile port hasn’t been done. The percentages describe the code as it stands and the class of change each part would need. They are not a report from a port that happened.

What we’d tell another team

Build a real page with your own system before you trust it. Not a demo, not a kitchen sink — something someone would actually have to ship. Ours needed seven components and produced eight concrete gaps, two of which we’d only guessed at.

Count your exceptions and give each one a delete condition. Four exceptions in one file is a system with known edges. The same four scattered across the page is a system that’s quietly stopped being one.

And assume the expensive failures will be silent. All three of ours passed every gate we had. The wrong icon rendered. The impossible screen tested green. The accessible name read differently than it looked. None of them were errors — which is precisely why they survived.

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 →

A design system nobody has built a page with?

We build a real screen with your system and hand back the list of what stopped us, in order, with the cost of each. We reply within one business day.

Get in touch

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

© 2026 Micro Tech, Sarajevo