Raptive · Design Systems · 2026
Design Systems Token Vitals
Auditing what a legacy platform’s code actually does, so a visual overhaul lands everywhere at once
In-depth case study · the work is ongoing
design-token-vitalsGitHubGrades a codebase’s design token layer, reports what it cannot prove, and ranks the fixes into a next move.Vitals, at a glance
What the audit returns first: eight vitals, four of them unproven. A hatched track is a reading not taken, never a score of zero. The graded rows, and why that distinction matters, are below.

Every component, ranked by reach
87 components, ranked by how much of the product each one actually touches.
Every surface was captured at both screen sizes and reviewed as one screen. The desktop and mobile versions of the same thing count once, and a component that repeats inside a screen also counts once, so the number beside a component is the share of the product that changes when it does.
Foundational controls and visual primitives.
- Button22%
- Form field16%
- Validation message16%
- Avatar9%
- Badge9%
- Search field9%
- Select / picker9%
- Selection control9%
Small functional groups built from atoms.
- Filter controls34%
- Reaction bar19%
- Author row16%
- Action group13%
- Feed tabs13%
- Profile tabs9%
- Data visualization6%
- Metric card6%
Reusable interface regions and complex modules.
- Top navigation88%
- Bottom nav30%
- Composer16%
- Activity list13%
- Post card13%
- Sidebar11%
- Admin navigation9%
- Review nav9%
Three components are platform-specific and the reach ranking is what proves it rather than assumes it: the bottom nav exists only on mobile, the sidebar and the right rail only on desktop. A contract written from the desktop screens alone would have shipped a component the mobile app cannot use.
Reframing the opportunity
The product did not have a consistency problem. It had a visibility problem: nobody could say which components a design change would reach until it shipped.
Raptive Community runs on Discourse, a Rails and Ember app whose code was started in 2013, before design tokens were a practice and before CSS variables existed in browsers. Decisions about type, color, spacing, corners and shadows had piled up across Discourse itself, Raptive’s own plugins, Sass variables and CSS variables. Every one of them made sense when it was made. Together they had drifted apart: change one foundational value and some components followed, some reached past it to the raw color underneath, and some read a mobile-only copy of the same value kept in a different file.
Then a rebrand was scheduled. New type, new colors, new buttons. This time the change had to land cleanly across the whole product. Finding out mid-launch that only some of the buttons had updated was not an acceptable way to learn.
- How might we see, before shipping, which components a token change will actually reach?
- How might we order a migration by evidence rather than by whoever asks loudest?
Three questions Figma could not answer
None of the three could be answered in Figma. Figma holds what we meant to build; the browser shows what actually shipped.
The audit
So I built the scanner. It reads the real codebase, not a token file someone assumed was accurate, and grades what it finds.
It follows how the Sass and CSS files import one another, then separates the code Raptive owns from Discourse’s own. It untangles every way a value can be written (plain values, groups, one token pointing at another, CSS variables) into one entry per real decision, and traces which files use each one. Those files are grouped into components; hard-coded colors are counted separately. Out comes eight graded vitals and a fix list ordered by impact. It works on any front end built this way, and it is open source: design-token-vitals.
One value, four names
Vital · Single sourceSame value. Four names. Nothing in the code connects them.
White appears 76 times across 20 files and answers to at least four different token names. The brand color has the same problem. Teams had each named the same decision separately, not knowing someone else already had. Renaming one leaves every other spelling of that decision untouched, and nothing in the code tells you which ones those are.
- --background-inverse
- --mobile-background
- --mobile-secondary
- --brand-primary
- --icon-brand-primary
- --mobile-primary-blue
- --raptive-primary-blue
The values are provably identical; whether they mean the same thing is not. The scanner will not merge them on its own. Deciding that a Google button and a mobile background are the same decision is a judgement call for a person, and the report says so rather than quietly combining them.
No shared contract
Vital · Tier integrityThe button pulls 26 different tokens, 74 times, from more than one file.
Some buttons read a named layer, some reach straight past it to the raw color underneath, and some read a mobile-only override. Not because anyone chose that. There was no agreed contract, and nothing standing between a deadline and a quick local fix.
The button, in evidence
369 rules across the product paint something button-shaped. Twelve of them say enough to actually draw the button.
So I drew every one of them from its own source. Each record carries the class name, the button as that rule paints it, the size and shape it works out to, the tokens it reaches for, and the file and line it came from. Then it sorts them by how much the code is willing to tell us.
This is a third instrument counting a third thing. The audit counted how often the button component uses a token; the reach ranking counted how many screens the button appears on; this counts CSS rules that paint a button anywhere in the product. 74, 22% and 369 are three different questions with three correct answers.
Most certain first.
The twelve records we can trust most, and the reason the rebrand could not simply change one color: a neutral pill, a gray icon control, a white surface with a gray outline, a red fill, a red outline, a purple, two greens. Every one is a button. No two agree on fill, corner or type, and each points at a different file.

The gray and outlined placeholders are a rule, not a style: they mean the evidence is incomplete, never that the button is gray. A record only gets a color once the source, or watching it run, actually says what that color is. It is the same refusal the eight vitals make. The tool would rather show a blank than a color it cannot prove.

That is fifteen of 294, and the proportion is the point rather than the parade. The other 279 look the same. Below them sit 68 more that are name-only: a real class, in a real file, setting no visual properties at all. Together they are why “change the button color” was never a one-line job. Twelve rules can be edited with confidence; the rest have to be worked out before anyone can say what editing them would do.
Two rankings
By how much code uses it, the button ranks first. By how much of the product you see it on, it ranks fourth, behind a navigation bar that appears on 88% of screens.
Both numbers are right, and they answer different questions. Counting uses in code tells you where the work is. Counting screens tells you where a mistake would be seen. The button is 8.7% of all token use and appears on 22% of screens. The top navigation appears on 88%. I have not measured how much code it uses, so I cannot tell you whether it is the cheaper fix, only that it is the more visible one.
The two also cover different ground, which is easy to miss and expensive to mix up. The code audit found 479 tokens across everything the product loads, Discourse included. The design- side inventory counts only the 118 tokens Raptive owns outright, with Discourse supplying the rest as the page loads. Neither is the “real” number. Quoting either one without saying which count produced it is how a slide deck ends up wrong.
Three groups, three different needs
The same 87 components group a second way, not by how complicated they are but by what each group needs from the token layer.
Operational controls is the group that matters most and looks least urgent. Those components run almost entirely on values Discourse fills in as the page loads, so they cannot appear in a Figma library at all until each one is pointed at the same named color, spacing, corner and type groups as everything else. Building the library before that happens would produce a design system that quietly leaves out the admin, review and settings screens.
Eight vitals, four unproven
The audit reports what it can and cannot prove about itself.
- Naming coherenceFour naming styles are in use where there should be one.fail
- Tier integritySome components skip the named token and use the raw value underneath.attention
- Coverage14 of 19 token groups were checked. Five were never measured.attention
- Single sourceThe same value answers to several different names.attention
- Leakage26 hard-coded colors match a token exactly; 163 more have no token at all.blocked
- Mode completenessThe built light and dark themes have never been checked.blocked
- OrphansWhich tokens nothing uses is still unknown.blocked
- EnforcementNothing in the build stops a hard-coded value getting in.blocked
Blocked is not a failing grade. It is the tool refusing to grade what it never measured. Four of the eight say “no evidence yet” rather than inventing a number. An audit that grades everything is not measuring; it is guessing with a scoring sheet.
Design, approve, migrate
Design somewhere the framework cannot argue back, get agreement on clean evidence, then migrate in the order the audit already proved.
One contract, two implementations
One agreed set of decisions. Two builds of it, each native to its own framework. Every output format generated, never kept up to date by hand in two places.
React reads the contract and produces React components. Discourse reads the same one and produces Ember and Glimmer components. Both end up looking and behaving identically without sharing a line of code. The CSS variables, the Sass and the Figma JSON are all generated from it, which is what stops the two halves drifting apart. That drift is what caused the mess in the first place.
Button / Primary
background color.action.primary
foreground color.text.on-action
height size.control.medium
padding-inline space.md
radius radius.control
focus-ring color.focus
states default · hover · pressed
focus · disabled · loadingThe library, drafted
The combined library is drafted: 909 tokens across 14 groups, in one export meant to become the single source both Figma and code read.
It holds the raw values and a temporary bridge back to Discourse in the same file, which is what will let the migration go component by component instead of all at once. The bridge is meant to be temporary. Every token in it marks a component still to be moved. It is not wired into production yet, and counting it as delivered would be counting the plan as the work.
Migration roadmap
| Phase | Why it sits here | Status |
|---|---|---|
| 1 · Buttons | The most used, at 8.7% of all token use. Not the most seen. See Two rankings. | Roadmapped |
| 2 · Typography | Everything after it sits on top of type. The DM Sans files are already confirmed from source. | Planned |
| 3 · Spacing & layout | Grid, gaps between components, padding inside them. Check the scale, then fill in its missing steps. | Planned |
| 4 · Color harmonization Vital · Leakage | Point the 26 exact matches at their token, sort the 163 with no match, and retire the duplicate names. | In discussion |
| 5 · Mode & accessibility Vital · Mode completeness | Build every light and dark pair, look at them in real screens, and check they pass WCAG 2.2 AA. | Next gate |
Buttons are the critical path: type and spacing both depend on the shape the button contract settles. The order is not a preference. It is measured use, ranked.
What changes
Locating a decision
Search 21 files, guess which components the change reaches, ship it and find out.
A ranked list of 105 components, with the file and line for every single use.
Shipping a visual change
The change reaches some components and not others. Discovered in QA, or after launch.
Components read one named layer, so a color change spreads for one reason instead of four.
Design against production
Figma and production drift apart until something ships wrong.
The audit draws the line between what is proven and what is not. Differences get written down, not discovered late.
What transfers
None of this is specific to Discourse, or to a rebrand. It needs only a codebase where one decision answers to more than one name.
Any change that has to land everywhere at once asks the same question first, whether it is a rebrand, an acquisition, or a design system’s second version. The scanner reads Sass and CSS, so it fits most front ends built on them, and it is open source. What it cannot do is read a value that does not exist until the page runs: a variable a theme changes after load is invisible to anything that only reads files. That limit is the same on every codebase, and knowing where it falls is part of using the tool rather than a flaw in it.
The part that transfers without any tool at all is the order you do things in. Measure the code rather than the design file, because the design file holds what you meant and the browser holds what shipped. Rank the work by something you measured rather than by whoever asks loudest. And write down what you could not prove, so the next person knows where to look instead of inheriting a scorecard that graded everything.
Role & scope
Strategy
- Argued the rebrand was a structural problem, not a visual one
- Defined one set of tokens that belongs to no framework
- Set the migration order from measured use, not from opinion
Execution
- Built the tool that reads the live codebase and grades it
- Built the React and Vite environment from nothing: 224 tokens, 45 components, checked automatically on every change
- Drafted the combined 909-token library, meant to become the one source Figma and code both read
- Wrote the button spec for every state and both modes
Leadership
- Planned the rebrand in phases with product managers, using the audit as the evidence
- Gave engineering shared words for tokens, layers, and values that escape them
- Wrote down what the audit cannot prove, so nobody claims more than it shows
What is proven, what is not
Proven
- 479 tokens mapped across the 21 places they are defined
- 105 components ranked by how much they actually use
- 26 hard-coded colors that match a token and 163 that do not, each found at its file and line
- DM Sans and 17 brand colors confirmed from the real files, not from a spec
- A migration order based on measured use rather than on preference
Still open
- The built light and dark themes have not been checked yet
- Nothing in the build stops a hard-coded value getting in
- Whether those 163 values actually mean the same thing still needs a person to decide
- 70 tokens are still unsorted
- Whether this makes teams faster is unmeasured; the starting point is being recorded during rollout
The audit fixes nothing. It shows the shape of what needs fixing and puts it in order, which is the part a rebrand cannot be planned without. A rebrand is normally a visual change. Making this one structural is what makes the next one cheap.