Design System

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. After I was let go I kept going and took it all the way into a packaged React library an AI can build from. Then I had it audited against a written standard. It came back bad and I published it anyway.

Role
First and only Product Designer at LettrLabs (14 months). Built 100% of the components, patterns, UX copy and tone. That meant the Figma library plus the AI validators for copy, component rules and PR checks, all built as prototypes rather than code
Built
On my own, took it into a fully-usable React package with machine-readable specs, agent skills, and a multi-agent QA pipeline
Status
On npm as @ivancreatelabs/design-system (v0.5.0). An engineer reviewed it and called it fit to build on. Still a personal project, not yet in production
Adopted
A Senior PM built three working prototypes straight from it, so it works for someone other than its author

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

Quick Summary

The problem

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

My part

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

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, then published the rest as open findings instead of quietly closing them.

# 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 Button, DataTable, MapCard… $ import { Button, MapCard } from "@ivancreatelabs/design-system"

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). Nothing on a screen is allowed to get those values anywhere else
3review stages. Write the standard, audit, then 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. It's a personal project and it isn't running in production anywhere.

It's real, not screenshots.

This is the installed library running live in your browser. Sign-in, billing, a pipeline dashboard, an org-scope editor, a map, a mail-merge print preview and more, all built from the same components and tokens. The fields type, the toggles flip, the layout reflows.

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

When I joined, LettrLabs had no design system at all. Figma was empty. 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, meaning type styles, tokens and components, and held all of 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 with one designer holding it, 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 and 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. That sounds inefficient, but the 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."

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 like brand/bg/default and 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, so the swatches are the live values and 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, because 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 in the fields.

The count hides something I should say plainly. 56 of the 95 are global primitives built as a system (Button, InputField, DataTable). The other 39 are product one-offs I pulled into one place, things like map tools, charts, org and campaign screens. Both are real components. Only one set was ever built to a standard, and the audit felt that difference.

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 turn into 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 comes 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 instead of papering over it.

UX copy · tone

Copy became a formula, not a guess

Every message follows the same pattern. What happened, what it means, what to do next. Give a model the state and it fills in 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 every one of them gets a decision. That's how a one-person system survives a growing team.

  • Violation breaks a rule → fix the design
  • Deviation has the right value but no token → fix the binding
  • New pattern is undocumented → name it, or replace it

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. The written rules decided, not my taste. 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 sitting where a token already existed. I'd have called this system consistent. Measured, it mostly wasn't.

Four checks the build now enforces, before and after.

Every row is a script that fails the build, so it can't quietly regress. Anyone with the code can run these numbers again.

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 kept 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.

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

The audit ships with the system, unfixed findings and all. What a system actually looks like beats what its owner says about it. Two categories are closed and enforced by the build. The other eight are open or partly open, and I'd rather name each one 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, because 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 that 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 so than call the audit complete.Open

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. It's a worked example of how I'd run a design system with engineers involved, and proof I can take one past the Figma file.

  1. PACKAGED

    Past the Figma file, into a package

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

  2. Proven beyond me

    Holds up in someone else's hands

    A Senior PM built three working prototypes straight from the same system. It isn't only consistent for its author. Someone who didn't design it could build with it.

  3. 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.

  4. Design that survives code

    Compiles, not just looks right

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

  5. 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 by any model. So the check comes first now, or the rule doesn't count.

  6. 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. That's a design problem before it's a tooling one.

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. There's one versioned source of truth that 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 the 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. If I can't hand someone a command they run for themselves, it isn't a finished rule yet. 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."