Skip to main content

Notes from building an AI design collaborator · part 6

We rebuilt our Figma design system out of the code, not the other way round

· 5 min read

Every design system I've worked on has the same quiet problem. The Figma file and the actual coded components drift apart. Someone tweaks a button in code, nobody updates the Figma one, and six months later the "source of truth" is a museum piece that lies to every new designer who opens it.

I kept coming back to one thought. The code is what actually ships. So the code should be the source, and the Figma file should be built from it. Not the other way round, and definitely not by squinting at the running app and redrawing it by hand.

The pipeline

Three parts, and they're all boring on purpose.

A token file. Colours, spacing, radii, type, shadows, written as W3C design tokens, which is just JSON with an agreed shape. Primitives at the bottom, semantic aliases on top, so surface/sidebar points at a specific black rather than repeating it. That format matters mostly because Figma variables can be fed from it directly instead of by hand.

A component spec. A declarative list of every component and its variants. The important field is the one that isn't about design at all: each entry names the real code component it corresponds to. That single line is what later lets the Figma-to-code mapping be generated from the same file instead of maintained as a second, drifting thing.

One generic builder. A script that reads both files and assembles the library through Figma's plugin API. It knows nothing about our product. Point it at a different spec and it builds a different system.

Because the whole thing is generated from one source, rebuilding it is cheap. The code moves, you run the build again, the Figma file catches up. The museum problem goes away, because nobody is maintaining a second copy by hand.

What came out the other end: 52 primitive colours, 34 semantic tokens, 17 spacing and radius variables, 14 text styles, 4 shadow styles. Then 31 components, and 149 icons.

"Real assets only" is where it gets uncomfortable

The rule I held to was that if it isn't in the code, it doesn't go in the file. That sounds obvious and it is the thing everyone skips, because drawing a plausible icon is faster than wiring up the real one. It's also how the drift starts.

In practice the rule meant three unpopular calls.

The icons are the actual SVGs from the codebase's icon map, converted and tagged as stroke, fill, or leave-alone so they can be tinted correctly. Not redrawn, not from a library that looks close.

The tokens are read from the framework config and the typography stylesheet, deliberately not from our own written design documentation. We have a perfectly nice document describing our colours. It was wrong in a couple of places, which is exactly why it can't be the source.

And where the coded component disagreed with the spec I'd written, the component won. Every time. The computed-style dump from crawling the running app was explicitly not an input either, because a crawl faithfully captures your bugs alongside your intent.

One build detail, in case you try this. The Figma write tool caps how much code you can hand it in one go, at around fifty kilobytes, and it can't read from your disk. So the library gets built as batches of self-contained payloads. Components went over in five batches, icons in chunks of about thirty. Not hard, just the sort of thing that ambushes you at 11pm.

What it got right, and what it didn't

The first real build came out about eighty percent right. I want to be honest about the other twenty, because that's the interesting part.

The machine nailed the mechanical stuff. Tokens, variants, the tedious matrix of states that nobody enjoys doing by hand. Later we ran a full re-audit with six agents working in parallel, each taking a slice of the component code, and they came back with a long list of real divergences: control borders pointing at the wrong token, a date field off by a few pixels, status colours that had quietly diverged between code and spec, a checkbox that had been blue in Figma and grey in production for who knows how long. All applied to the spec and the file together.

Where it needed me was judgment, and the three that stick in my memory are all naming problems.

There was a Pagination component in the spec that had no source in the code at all, so it came out. There was an Avatar that wasn't an avatar. The real component renders a currency flag, and calling it Avatar had been quietly teaching every new designer the wrong mental model, so it's Flag now. And a checkbox had a Partial state in Figma that doesn't exist in the product, which is the kind of thing a designer draws because the matrix looked incomplete.

None of that is in the code, because it isn't a fact about the code. It's a call about what a thing is, and which two things are the same thing wearing different names, and how to group all of it so a human can find anything.

So the model I landed on isn't "the AI builds the design system." It's "the AI generates it from the source of truth, and a human reconciles the parts that need judgment." The deterministic pipeline does the heavy, boring, error-prone work perfectly. The human does the twenty percent that's actually design. Either one alone is worse than the two together.

That split, generate from the source then reconcile by hand, has turned out to be the shape of most of the useful AI work I do now. This was just the first place I saw it clearly.


Part of a series on building an AI collaborator for our design team at Xflow. Each post stands on its own.

  • design-systems
  • figma
  • ai

The work behind the series

Designing the Instructions

This post is one thread out of a three-month project: an AI design collaborator for a payments team. The case study is the whole of it: what worked, what broke, and what is still unproven.

Read the case study →