Skip to content
A single teal line on a dark grid runs in from the left to a bright dot, then branches into three lines leading to three boxes stacked one above the other; the middle box is filled, the top and bottom ones are outlines.

“Conventions are not documented. They are enforced.”

Five problems we kept running into, and the system that answers them

Eleven projects taught us the same five failures. CDS is what we built so we stop paying for them.

Get in touch
Own product, no client · 6 months, ongoing · in use in our financial platform and in this site
74
components, three targets
430
tests across 55 files
~2,450
lines that fail the build
Summary

Across eleven projects we hit the same five failures. Web and mobile start identical and share nothing but a name two years later. Figma is one source of truth and the code is another, and nobody knows which is correct. Conventions live in a README and are followed until someone is busy. Every new product starts its component layer from zero. And any product sold under someone else’s brand either has tokens or it has thirty unmaintainable copies.

CDS answers all five. Design tokens arrive from Figma automatically over an MCP server — tokens, not components. A theme token resolves per platform — a CSS variable on web, a sentinel the native resolver replaces — so the platform split lives one layer below the call site instead of leaking into it. And the conventions are not documented, they are enforced: ~2,450 lines of verification that fails the build.

74 components, 430 tests, an 81-page catalog generated from source.

The problem

The usual tools make divergence free. Platform.select puts a branch wherever it’s convenient; conditional imports let any file diverge. Neither leaves a trace, and nothing fails when they multiply.

The constraint

The system has to stay correct after we stop paying attention. A convention with no owner is a suggestion: it survives while someone has time to review carefully, and stops the first week someone doesn’t. So the invariants need an owner that fails the build.

What we did

  1. Pushed the platform split one layer below the call site

    createTheme() turns a token into a string that resolves per platform, so ${theme.color.fg.default} behaves identically everywhere. Shared recipes sit above it, applied by both platforms, differing only in the trigger — :hover against pressed. There is one definition of a color, so colors cannot diverge.

    Login call byte-identical on 3 targets
  2. Brought tokens from Figma automatically

    Over an MCP server, with a manual path as fallback. The token layer is a package — tokens feeds core, which feeds ui — so a change has one entry point rather than a search-and-replace. This covers tokens, not components: palettes, scales and theme values flow from Figma, components are written in code.

    Tokens, not components
  3. Gave the invariants an owner that fails the build

    verify-tokens (902 lines), verify-conventions (1,056) and verify-katalog (485), plus an ESLint plugin with four rules. One rule carries the architecture: a pattern must not have a .native variant. CI calls that chain plus three real builds — Next, Vite and Metro — because next build is the only place the 'use client' rule is actually verified.

    ~2,450 lines of verification
  4. Shipped it as a package already in use

    In our financial platform — and in this site. Coverage is a full set rather than a starter kit: primitives, forms, overlays, navigation, data. Measured: sideEffects: false took an entry importing only Box from 253,075 B to 241,309 B. The trap is documented next to it — @cds/tokens must not be marked false, or reset.css is dropped and the layout collapses without a single error.

    253,075 B → 241,309 B
  5. Let no component know its own color

    Only a token name. A new brand is a new set of values, not new code. That was proven on an earlier product carrying more than thirty white labels; here it is the architecture from the first commit rather than a rescue.

    30+ labels, no new code
THE PROOF — ONE CALL, THREE PLATFORMS
b768aa14a346
apps/web — Next.js 16, RSC
b768aa14a346
apps/spa — Vite SPA
b768aa14a346
apps/mobile — React Native 0.87
135
lines, one file, no native twin
The whole login screen is assembled from Box, Button, Card, Input and Text — the split happened in the primitives. The call is identical; the wrapper stays platform-specific.
WHERE THE TWINS LIVE — 38 OF 74
primitives 27 twins

Leaves translating a single platform element and nothing more. This is where a split belongs: mechanical, contained and invisible from above.

33 of the 38 twins import no other component. They are leaves translating a single platform element — div to View, input to TextInput — and nothing more.

The result

74 components and 430 tests, an 81-page catalog with props tables generated from source, and not one pattern without a shared implementation. It is in use in our financial platform and in this site — if you want to know whether it works, you are looking at it.

What we’d do differently

We reviewed all 74 components and found 188 issues, which reduced to three root causes — and every one of them passes the entire toolchain. That review should have happened at 30 components, not 74: the same causes were already present and would have been an order of magnitude cheaper to correct. All 188 are fixed; aria-label and tabindex coverage is still open.

The method and the three causes are written up in a separate blog post.

Related
All 18 case studies
  • TypeScript
  • Next.js 16
  • React Native 0.87
  • Vite
  • styled-components
  • pnpm workspaces
  • Figma MCP
  • custom ESLint plugin

Web and mobile already drifting apart?

Get in touch

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

© 2026 Micro Tech, Sarajevo