Figma's left sidebar, showing the layers panelFigma's right sidebar, showing design properties
NEW Figma plug-in · Design systems · Tokens

Theme Runner

A Figma plugin that audits every tokenised value in a design system, then themes them all in a single approved pass.

The Theme Runner plugin window open over the Figma canvasFigma's floating toolbar

Theme Runner in 60 seconds

Role
Designer and builder, end to end
Tools
Figma, Tokens Studio, GitHub, Claude
Status
v1.2 — in testing with the design team

01 — The problem

Every brand delivery meant applying 5 themes across 40 components by hand — about 200 identical actions each time. Tokens Studio writes raw values onto layers, so the link back to the source token looks lost.

02 — The insight

The link wasn't lost. Tokens Studio already stores each applied token's name in Figma's shared plugin data. Nothing was reading it.

03 — What I built

A plugin that reads that data, audits every layer value against its token, runs the full theme as a dry run, and writes only after explicit approval. Covered by 467 automated checks across five test suites.

04 — What the first audit found

19,851 of 90,222 values — 22% — did not match their tokens, and 1,497 tokens did not resolve at all. Stale values that had been shipping undetected.

05 — Where it stands

100% accuracy verified on two components, at 1,105 and 12,920 values. Resolutions cut roughly 97% by memoising, from about 9,600 to 310 per page. Next: the first live themed delivery, measured against the 200-action baseline.

What this case study covers

confidential — partially withheld

This was built for a live client design system, so parts of it cannot be shown. Component names, token names and brand colours have been changed or removed throughout, screenshots are redacted where they would identify the work, and some figures are deliberately held back. Everything that remains is accurate and measured — nothing here has been rounded up, estimated, or invented to fill a gap. If a number would need context I cannot share to be honest, it is not on this page.

The brief

Every time an agency brand file landed, I had a colour palette and a type specification to apply across an entire component library.

The process was mechanical. Paste the values into Tokens Studio. Then, for every artboard: select it, switch the theme in the plugin, hit Apply. Forty components, five themes each. Around 200 repetitions of the same three actions, for every brand delivery.

I was asked for a way to automate the process.

The component library open in Figma, showing themed artboards on a single component page

The working file we ran against.

The repetition which had a cause worth finding.

Tokens Studio was writing raw values onto the layers. A hex code has no memory of the token that produced it. Once a colour is sitting on a layer, nothing records which token it came from. So every re-theme had to re-walk every artboard and re-stamp every value from scratch.

That is why the cost was components × themes, and why it repeated in full every single time.

Automating the clicking would only have made 200 operations happen faster. The better question was why 200 operations were needed at all.

Before — once per artboard

Select the artboard

→

Switch the theme

→

Apply

↵ repeat ×200

Forty components, five themes each — and the whole loop again on the next brand delivery.

After — once per brand

Paste the palette

→

Audit

→

Run once

→

Review

No repeating now. The loop existed because painted values forget the token that produced them — once that name is read back, there is nothing left to repeat.

The obvious answer, and why it died

Figma variables and modes, the route the team had already tested and set aside

The variables route the team had already tested.

The textbook fix is Figma variables. Bind layers to variables, put each theme in a mode, and a theme switch becomes a mode switch.

My team had already tried variables and rejected them as hard to manage at our scale. That wasn't my call to overturn, and arguing for it would have spent the goodwill I needed for whatever came next.

So the constraint became the brief: solve this without variables, without changing how the file is built, and without asking anyone to relearn their workflow.

The thing that was already there

Tokens Studio stores the name of every applied token on the layer itself, in Figma's shared plugin data. It publishes this deliberately, so other tools can build on it.

Which means every component in the library was already labelled — the token name sitting on the layer, surviving every re-theme, invisible to everyone.

The automation didn't need new infrastructure. It needed to read what was already written down.

2,335

layers on a single component page were already carrying the name of the token that painted them — counted by Inspect, before a line of theming code existed.

How it comes back out

node.getSharedPluginData('tokens', key)

One documented call per layer. Tokens Studio writes this deliberately and publishes the format, precisely so other tools can build on it — which is the only reason any of this was possible without touching how the file was built.

Tokens Studio documentation for shared plugin data, describing the token name stored on every layer

Tokens Studio docs ↗

Every layer already knew which token produced it. Tokens Studio documents the format — open the docs.

How it works

1
Audit — read every layer's stored token assignment and compare it against the value actually sitting on the layer
2
Set the rules — confirm which theme each artboard belongs to, set the scope, and exclude anything outside the theme system
3
Dry run — resolve every token and build the complete list of intended writes, without touching the file
4
Apply — write the resolved values, and only on your explicit approval

Same output as the manual process — raw values on layers, identical file structure. No migration, no rebuilt components, nothing for the team to relearn.

A — Verify

The Verify tab reporting values compared, identical, different and unresolved

Compare what every layer should be against what it is. Nothing is written, and a zero comparison count reports as a failure rather than a pass.

B — Dry run

The Apply tab open in dry run mode, listing the full plan of intended writes

Apply opens disarmed. The full plan of intended writes is resolved and listed, and the button only turns red once you deliberately arm it.

C — Applied

The full Figma interface with a theme applied across the component page

The same output as the manual process — raw values on layers, identical file structure — reached in one approved pass.

Four problems the real file threw up

Five artboards, one name

Every component page had five artboards named identically. The theme lived in the designer's memory, not the file. So the plugin scans the whole file and identifies every artboard instantly, then resolves each one against a rule set the designer defines — matching artboards to theme sets, and offering to apply. Nothing in the file is  restructured; the knowledge moves out of memory and into rules the team controls.

Components inside components

Roughly 85% of the tagged layers — 2,119 of 2,335 — sat inside nested component instances. Workable, but it means the writes are override operations, which behave differently and can be refused.

Gradients

Some colour tokens held CSS gradients rather than flat colours, in a non-standard syntax with inconsistent spacing. These needed parsing into real Figma gradient fills, including converting CSS angles into Figma's transform matrix.

Documentation furniture

Themed artboards also contained annotation labels using tokens outside the theme system. These had to be excluded explicitly, rather than allowed to block a run.

Two engines, one panel

The plugin panel reconfigured for the token format it detected in the loaded files

Both formats identify themselves in their own contents, so there is no format setting to get wrong — the panel reconfigures around whichever it finds.

The constraint was never to avoid Figma variables. It was never to rebind the client's file to them — and reading an export is a different act.

A later version added a second engine that reads Figma's own variables export alongside Tokens Studio's. Figma writes one JSON file per mode, and the reconciliation is the interesting part: five mode files each carrying the same variable name become one variable with five mode values, not five separate variables.

Apply creates any missing collections, modes and variables, then makes a second pass so an alias reference resolves to a real Figma alias rather than a string — semantic tokens stay wired to their primitives instead of quietly flattening into copies of them.

30 / 30
mode values reconciled correctly, tested against the real exported files rather than fixtures written to pass.
2
token formats read, with no format setting for anyone to get wrong.

Neither format has to be declared. Both identify themselves in their own contents, so the files are read as they land and the panel reconfigures itself around what it found.

The thing nobody asked for

To trust the tool, I built a mode that compares what it would write against what is already on every layer — before writing anything. Then I ran it across the entire library.

90,222

values compared in a single run, across every component in the library.

70,371

identical — 78% of the library already matched what its tokens say it should be.

19,851

different — 22% of everything checked had drifted from its own token.

1,497

unresolved — flagged, never guessed, and each one blocks a strict run until it is settled.

Nearly twenty thousand values across the library did not match the tokens that were supposed to have produced them. Not a bug in the tool — components that had drifted across many manual re-theming passes, with nothing in the pipeline able to see it happening. The pages already corrected come back clean; the 22% sits in everything that had not been touched.

The 1,497 unresolved are a different problem, and the more urgent one. Strict mode means a single token that fails to resolve stops the whole run, so each of those has to be settled before a full apply can go ahead. The tool had accidentally become an audit — it is the only thing in the pipeline that knows what a layer is supposed to be, so it is the only thing that can say where the library has slipped, ranked worst-first by page.

The business case shifted. “Saves me days per brand” is useful. “Saves me days per brand, and found thousands of wrong values we had been shipping” is much harder to argue with.
The all-pages Verify result: 90,222 compared, 70,371 identical, 19,851 different, 1,497 unresolved

The whole library measured at once, and the drift visible for the first time.

The per-page breakdown ranking every component by how far it has drifted

A theming tool that became a health report.

Three bugs worth admitting

A case study where nothing goes wrong reads as fiction. Each of these changed how the tool was built, not just what it did.

The check that lied

An early Verify run reported “0 values compared” — and rendered it green, because the code asked whether the mismatch list was empty and got the answer it wanted. Nothing had been compared; the scope was empty. It now reports Nothing was compared — this is not a pass in red, with a decision tree naming which of the four possible causes it was.

The gradients that were never written

Dry run listed gradients in the plan. Apply reported success. No gradient changed. The write filter was a map of property kinds and it had no gradient key — an absent key is falsy, so every gradient was dropped in silence. The one-line fix mattered far less than the test that now asserts the map covers every kind the resolver can produce.

The test that tested nothing

A check that a tested colour was actually present in the shipped stylesheet passed. Then I deleted the code it was guarding, and it passed again — the colour was also named in a comment explaining why it had been chosen. It strips comments before looking now.

Every guard in this project was verified by breaking the thing it guards and confirming the test fails. A test that does not fail when its subject is removed is decoration.

The gradient one I got wrong twice. When the designer reported gradients not applying, I said I did not think I had ever claimed gradient support. I was wrong — their colour tokens held CSS gradient strings, and the tool had been quietly mishandling them the whole time.

Proving it rather than trusting it

Three deliberate choices, because a theming tool that is subtly wrong is worse than no tool at all.

Verify before Apply

The comparison mode above. On a correctly-themed file it should report 100% identical — meaning the tool independently arrives at exactly what Tokens Studio produced.

Strict by default

If a single token fails to resolve, nothing is written at all. A half-themed file is worse than an untouched one.

Guards that can fail

A guard that cannot fail is decoration, so each one was checked by breaking the thing it protects and confirming the test went red. The theme detection was validated the same way — three artboards deliberately mis-themed, and it caught exactly those three.

Before — the check that lied

0 values compared

Passed

The code asked whether there were any mismatches. An empty scope has none — so it got the answer it wanted, and rendered it green.

After — a check that can fail

0 values compared

Nothing was compared — this is not a pass

Zero compared proves nothing. The same run now fails, and names which of the four possible causes left the scope empty.

The same result, read two ways. A check that cannot fail is not a check — it manufactures confidence rather than earning it.

Interface decisions

A tool that writes to a production library needs its danger to be legible.

Cool colours read, warm colours write

Blue and gold cannot change your file; red can. You do not have to read the label to know whether a click is safe.

The destructive action looks destructive

Apply opens in dry-run mode, and only turns red — with a slow pulse — once you deliberately disarm that.

Every button tested for contrast

I ran both WCAG 2.1 and APCA across every state, composited over light and dark backgrounds. My first palette used the system colours; the success green measured 2.10:1 against a 4.5:1 requirement. Every fill was reworked until it passed. The suite stayed, and later caught a second one — a version chip whose grey text on a grey fill measured 3.44:1 in light and 4.03:1 in dark. Subtle and invisible are different things.

Image 07 — to be added

The four intent colours side by side — blue, gold, red, green — with measured contrast values overlaid.

34 contrast checks, across every button and glyph, in light and dark.

Two files Figma interprets

No build step, no dependencies, no framework — a manifest and two source files. A plugin nobody can update for you is one you should be able to read end to end.

467
automated checks across five suites — resolution, variables, run control, security, contrast.
~97%
fewer token resolutions after memoising — around 9,600 down to around 310 on one page.
1,360
values in a single apply plan — 930 colour, 255 typography, 175 numeric.

The resolver and variables suites don't test a copy of the logic. They extract the shipped library sections out of the source and run those, so a test passing means the code that ships passes. The variables suite runs against five real exported mode files kept as fixtures, rather than fixtures written to agree with the parser.

Where it stands

Verified across two components — 1,105 and 12,920 values, both 100% identical. Currently in a month of testing before it reaches the wider team.

What is still unknown

No live write has ever run — on either engine. Every Apply so far has been a dry run: no value overwritten, no variable created in a real file. Nearly all writes land inside component instances, which Figma sometimes refuses, so the failure count on the first real run is genuinely unknown. That gets settled on the first live apply, not before.

What I took from it

The brief I was handed described a repetitive task. The actual problem was a data model that threw information away — and the answer was recognising that the information had never been thrown away at all.

It was in the file the whole time. Published, documented, and unused.

The second lesson was about proof. It would have been easy to run the tool once, see the file look right, and call it done. Building the comparison mode first is what surfaced four thousand stale values that had been shipping for months.

Client name, component names, token names and brand colours have been anonymised throughout, and some figures are withheld under client confidentiality. Every figure that does appear is a measured value from a Verify or Inspect run on the real file. Happy to talk through the withheld detail in person.

← Back to all projects
↑
You're back at the top