Design systems · Self-directed project
Personal project · v0.5.0

Design to code, both directions.

I was LettrLabs' first and only Product Designer. There was no design system, so I built one from raw hex in Figma — then, after I was let go, took it all the way into a packaged React library an AI can build from. I had it audited against a written standard; it came back bad, and I published it anyway. That's the part worth reading.

RoleFirst and only Product Designer at LettrLabs (14 months). Built 100% of the components, patterns, UX copy and tone — the Figma library and the AI validators for copy, component rules and PR checks — as prototypes, not code
BuiltOn my own, took it into a fully-usable React package: machine-readable specs, agent skills, and a multi-agent QA pipeline
StatusOn npm as @ivancreatelabs/design-system (v0.5.0). Engineer-reviewed as fit to build on — a personal project, not yet in production
AdoptedA Senior PM built three working prototypes straight from it — proven beyond its author
# it's on npm $ npm install @ivancreatelabs/design-system # load the tokens once, at your app root $ import "@ivancreatelabs/design-system/tokens.css" # then build with it — Button, DataTable, MapCard… $ import { Button, MapCard } from "@ivancreatelabs/design-system"
At a glance

95 components, one written standard, and a grade I didn't choose.

56 + 39global primitives plus product-specific components — 95 in all, each documented and under test
439generated design tokens (colour, dimension, type, elevation) — the only place those values are allowed to come from
3review stages — write the standard, audit, repair — with one agent per component per stage
705lines in the component standard all three stages worked against
857findings raised by the agent audit, sorted into ten categories
76components graded one by one against the written standard — the other 19 are still ungraded

The counts come from the repo's own scripts, not from me. This is a personal project — it isn't running in production anywhere.

The question

A Figma library and a codebase drift apart from day one. I'd built the Figma half and written the rules, but never had to defend either against a compiler — so I wanted a straight answer: is the system actually consistent, or does it just look that way in Figma?

What I did

Rebuilt the library in React on three-tier tokens, wrote the rules in a format an AI can read, and packaged it with versions and releases. Then a three-stage agent pipeline graded 76 of 95 components against the written standard; 19 remain explicitly ungraded.

The outcome

Blunt: not one component got an A, and the worst category was the rule I was most sure of. I fixed everything a script could catch and published the rest as open findings — rather than quietly closing them.

Main UX

It's real — not screenshots.

The installed library running live in your browser. Real product surfaces — sign-in, billing, a pipeline dashboard, an org-scope editor, a map, a mail-merge print preview and more — each built from the same components and tokens — the fields type, the toggles flip, the layout reflows.

Every surface is a live instance from the installed package — nothing here is a picture.

Where it started

Before there was a system, one screen could show five "primary" buttons.

When I joined, LettrLabs had no design system at all. In Figma there was nothing — raw hex colours, no text or type styles, no components, and no designer had ever owned it. In the product it showed as 10 button styles, 4 table patterns, up to 5 "primary" buttons competing on a single screen, no documented UX process and no voice. On the screens that drove revenue, buttons fought for the same click.

So I built the system, and I built it with the people who'd live in it. I worked directly with the CTO, the CEO and the engineers to pin down the primary action on each screen, the core flows and the feel. From raw colours up, I built the Figma foundation — type styles, tokens, components — and held it to one rule: same job, same pattern. An orders table behaves like every other table. One primary action, one CTA. A mental model you learn once and reuse everywhere.

Here's the honest part: even as the sole designer, the system still drifted and regressed — we rebuilt the library and workspace twice in fourteen months. Rules that live in one person's head, even mine, don't hold — so they had to become machine-checkable. That's the bridge from the Figma work to the AI validators, and later to the lint-enforced package. After I was let go, I finished the job: I rewrote the whole system as a packaged React library with rules a script — or a model — can enforce. The rest of this page is that system.

"We spent a week arguing about one button. It sounds inefficient — but that decision became the criteria every component after it was measured against, and the rest went fast. Working with engineers who think in systems, my job wasn't to control the outcome — it was to find the decision they already wanted, and make it consistent."

Act I · Architecture

The 439 tokens are layered so meaning drives the UI, and named the same way every time: category, role, intensity.

Layer 01 Primitive Every colour, size and radius the brand owns, with no meaning attached yet. Nothing on a screen is allowed to use these directly.
Layer 02 Semantic Roles point at primitives — brand/bg/default, neutral/text-icon/strong — so what you meant stays the same even when the value under it changes.
Layer 03 Component Each component only uses semantic tokens, so a Button or a Toast is never tied to a raw hex or pixel value.
BrandReserved for the user's positive action
NeutralText, icons, surfaces, borders
InfoSelection and information — blue, not brand
SuccessConfirmed and completed states
WarningCaution without failure
ErrorBlocking failures only
SpecialVariable images and mail merge only

Rendered straight from tokens.css — the swatches are the live values, not a diagram of them.

Brand is reserved. It marks the thing you want people to do — primary button, active tab, current step, focus ring. Never body text, never decoration. That one rule does most of the work in keeping a screen calm.
Dimension scales aren't interchangeable. Spacing, padding, gap, radius, border, height, icon size and opacity are separate scales, each used only where it belongs. Merging them is how a system rots.
Focus is a 4px halo, no exceptions. The same focus ring on every control, so the whole thing reads as one product instead of a pile of widgets.
Disabled uses lighter tokens, never opacity. And selection on small controls is info-blue, not brand — brand stays reserved for the action you want people to take.

Forty of the ninety-five, each a real interactive instance from the installed package — flip the toggles, type the fields.

Worth saying plainly, because the count hides it: 56 of the 95 are global primitives built as a system (Button, InputField, DataTable); the other 39 are product one-offs — map tools, charts, org and campaign screens — I pulled into one place. Both are real components; only one set was ever built to a standard. The audit felt that difference.

Act II · The machine-readable layer

If a model can't follow the rule, it wasn't written well enough.

Writing the docs for an AI made them better for people too — you can't tell a model to "use appropriate spacing," so every rule had to become something checkable, or it got cut. Four things carry the system to whoever, or whatever, builds next.

llms.txt

Docs read in a fixed order

The front door names the library and the order to read the rest in, so an agent can't guess halfway through. The components.json index is generated from the code, so it never goes stale.

docs/SKILL.md

The agent contract, five rules

The core rule first: search for what already exists and reuse it before making anything new — and never hard-code a colour, size or radius where a token exists. Flag what's missing; don't paper over it.

UX copy · tone

Copy became a formula, not a guess

Every message follows a pattern — what happened, what it means, what to do next. Give a model the state; it fills the blanks.

  • System failure → "We couldn't [action]. [Next step]. If it continues, [fallback]."
  • Validation → "[Item] is required. [Action] to continue."
docs/QA-FINDINGS.md

Every finding is one of three things

Not every difference is a mistake, but each gets a decision — how a one-person system survives a growing team.

  • Violation — breaks a rule → fix the design
  • Deviation — right value, no token → fix the binding
  • New pattern — undocumented → name it, or replace it
Act III · Multi-agent QA

I'm the worst judge of my own work — so I wrote a testable standard and ran an agent audit.

I couldn't audit 95 components by hand, and I didn't trust myself to. So I ran a three-stage agent audit—write the standard, grade 76 components against it, then fix—and let the written rules, not my taste, decide. Nineteen components remain explicitly ungraded.

A0 components
B3 components
C32 components
D41 components

Not one A. The biggest failure was the rule I was surest of — raw values where a token already existed. I'd have called this system consistent; measured, it mostly wasn't.

What changed

Four checks the build now enforces — before, and after.

Every row is a script that fails the build, so it can't quietly regress — numbers anyone with the code can reproduce.

CheckBeforeAfter
Refs that attach to a DOM nodeIf a ref doesn't attach, the app around it can't focus, measure or animate that component. 7795
Hardcoded values left in the codeRaw hex, rgba and font stacks sitting where a token already existed. 360
Components with a manifest entryThe old one was maintained by hand, so it fell behind the code. The new one is generated from the code itself. 5095
Components that render under testThe one number that didn't need fixing. It was the floor everything else got measured from. 9595
Terminal output: npm run smoke reports 95 of 95 components rendering; npm run check:tokens reports 439 tokens defined with zero violations; npm run check:refs reports all 95 components attaching a ref.
The limits

What the audit found that I haven't fixed yet.

The audit ships with the system, unfixed findings and all — what it actually looks like beats what its owner says. Two categories are closed and enforced by the build; the other eight are open or partly open, and I'm naming each rather than call the audit complete. Anything without a check behind it drifts back, whatever I claim today.

CategoryWhat the audit foundStatus
semantic-tokenRaw values where a token exists. The biggest category in the audit, and now the most strictly enforced — the lint fails the build.Closed · enforced
refRefs not forwarded, or forwarded then dropped before they reach an element. Every component now gets rendered and checked.Closed · enforced
a11yLabels, error announcements and icon labelling are fixed and checked. Keyboard navigation, focus trapping and expanded/selected state aren't — several menus and overlays still can't be used properly from a keyboard.Partly open
dynamic-dataComponents shipping hardcoded sample data as a default, or missing their loading, empty and error states. The data table is fixed; the rest aren't.Open
props-apiNames that don't match sibling components, missing passthrough, and props leaking onto DOM nodes.Open
controlledComponents that handle controlled or uncontrolled use, but not both properly — a parent sets a value after mount and nothing happens.Open
state · consistencyStates you can't reach or can't tell apart, and components solving the same problem differently from their siblings.Open
ssrA few components touch window or document outside an effect, which crashes server-side rendering.Open
no tieringThe manifest lists all 95 flat — nothing in the data tells an agent a map control isn't a reusable primitive, so it invites reuse of one-off product components everywhere.Open
never auditedNineteen components never got graded. They pass the automated checks, but nobody has actually read them yet — and I'd rather say that than call the audit complete.Open
What this is evidence of

What I'd bring to a team that has this problem.

This never shipped to customers, so I'm not going to claim a business result. What it is: a worked example of how I'd run a design system with engineers involved, and proof I can take one past the Figma file.

01 / PACKAGED

Past the Figma file, into a package

Most design-system work dies at the Figma file. Mine is an installed, versioned npm package — tokens plus 95 components — that a team could build on today. That's the half I'd never taken a system through before.

02 / Proven beyond me

Holds up in someone else's hands

A Senior PM built three working prototypes straight from the same system, so it's not just consistent for its author — someone who didn't design it could build with it.

03 / RIGOR

Willing to be graded

I ran an agent audit against a written standard and published what it returned—open findings and all—instead of the version that flatters me.

04 / Design that survives code

Compiles, not just looks right

I can hand an engineer tokens and components that forward refs and pass a lint — not a Figma link and a conversation about what I meant.

05 / Rules that enforce themselves

Write the check first

The rules that survived the audit were the ones a script could check; the ones I'd written as good taste got broken everywhere, by me as much as any model. So the check comes first now, or the rule doesn't count.

06 / AI, constrained not trusted

Build from what exists

Give an agent a readable spec and a reuse-first rule and it builds from what's there. Give it neither and it invents — a design problem before it's a tooling one.

What's next

Everything I claim should be a command someone else can run.

The open list is the roadmap. Keyboard and focus first, because that's the one where a gap actually locks people out instead of just looking messy. Then the props API, so components stop surprising the people building with them.

The package is why any of this holds: one versioned source of truth a fast prototype can pull from and a team can build on — so a fix travels both ways instead of dying in a single screen, and a demo and production stop drifting apart. That's the part an HTML mockup can never be.

So the work now is narrow: turn more of the open list into checks, and drop any rule I can't enforce. A claim I can't hand someone as a command they run for themselves isn't a finished rule — it's a hope with good intentions.

"A design system earns its keep when the same rules produce the same answer — whether a designer or a model is doing the work."