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.
Company
Raptive
Role
Staff Product Designer
Duration
2026, in progress
Scope
Token audit, semantic contract, and migration roadmap for Raptive Community
Team
Sole designer, partnered with Product Management and Platform Engineering · Discourse on Rails and Ember

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.

A detail from the platform audit board showing ten member-facing surfaces (Feed, Latest, For you, Categories, Community hot, Community latest, Topic, Tags, Search and Groups), each captured at desktop and mobile, annotated with the components it uses, and with those components isolated beneath it
Twenty of the audit’s 64 columns: ten surfaces at both screen sizes. Feed, Latest, For you, Categories, Community hot, Community latest, Topic, Tags, Search and Groups. Each screen is marked up with the components it uses, and has those components pulled out beneath it. 32 surfaces, 64 screen and device columns in all.

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.

Atoms

Foundational controls and visual primitives.

  • Button22%
  • Form field16%
  • Validation message16%
  • Avatar9%
  • Badge9%
  • Search field9%
  • Select / picker9%
  • Selection control9%
Molecules

Small functional groups built from atoms.

  • Filter controls34%
  • Reaction bar19%
  • Author row16%
  • Action group13%
  • Feed tabs13%
  • Profile tabs9%
  • Data visualization6%
  • Metric card6%
Organisms

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.

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?

None of the three could be answered in Figma. Figma holds what we meant to build; the browser shows what actually shipped.

01What do we actually have479 tokens are defined somewhere in the code. Which are real decisions, which are copies of each other, and which are used by nothing at all?
02Why doesn't consistency holdChange a value and some components pick it up. Why the rest ignore it is written down nowhere.
03How do we migrate safelyIf the whole product has to change, which components move first, and what makes that order the right one?

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.

479Separate token decisionsOne entry per decision, after every duplicate definition was merged.
21Places tokens are definedSome written by hand, some generated, and some disagreeing with each other.
4Naming styles in useCore color, Raptive color, typography, layout.
110Files the scan could followPlus 89 more it could not confirm, held as maybes or local overrides.
105Components that use tokensFound across 354 component and style files.
36.7%Of all uses, from five tokensThe impact is concentrated, which is what makes an order possible.
26Hard-coded colors with a matchThe exact value already exists as a token.
163Hard-coded colors with no matchNo token exists for these values yet.
Vital · Single source

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

#ffffff76 occurrences across 20 files
  • --background-inverse
  • --google
  • --mobile-background
  • --mobile-secondary
#6b65ff
  • --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.

Vital · Tier integrity

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

74Token referencesIn one component.
26Distinct tokensFor one visual decision.
2+Source filesA named layer, the raw values under it, and a mobile override.
8.7%Of all token use in the codeCounted by uses in code, not by how many screens it appears on.

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.

12Rules that fully describe oneEnough detail to draw the button truthfully.
294Rules that describe part of oneThe fill, the shape or the text comes from somewhere else, or never resolves.
68Rules that describe nothingA real class name that sets no visual properties at all.
369Rules that paint a buttonAcross 187 source files, narrowed from 552 candidates.

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.

Twelve highest-confidence button records, each rendered from its own CSS rule with its selector, geometry, tokens, and source file and line
Tier 1: the twelve records where the source says enough to draw the button truthfully. Each carries its class name, the size and shape it works out to, the tokens it reaches for, and the file and line it came from.

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.

A sample of the partial button contracts: rows of records whose fill, geometry and type are marked inherited or unresolved
Tier 2: fifteen of the 294 rules that describe only part of a button. Almost every panel reads “partial contract / inherited / unresolved”, and most name no token at all: no token reference in this rule.

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.

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.

118Explicit Raptive tokensThe ones Raptive owns outright.
78ColorNamed, brand, mobile, interaction and feature groups.
23LayoutSpacing from 2px to 128px, radius from 8 to 32.
17TypeSeven sizes, four weights, six line heights.

The same 87 components group a second way, not by how complicated they are but by what each group needs from the token layer.

01Shared shellHeader, sidebar, community items, icon buttons. These take their spacing, type and color from the platform's own defaults.
02Social contentTopic and discussion cards, question cards, reaction controls and summaries, avatars. These add tokens of their own for avatars, corners, outlines and reactions.
03Operational controlsSelect and filter controls, admin navigation, report tiles, profile stats and badges. These run on values Discourse fills in as the page loads, so each one has to be pointed at a real token before any Figma library work can start.

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.

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 somewhere the framework cannot argue back, get agreement on clean evidence, then migrate in the order the audit already proved.

01Design in isolationA separate React and Vite build, away from what Discourse allows. 224 tokens, 45 components, checked automatically on every change, built in four weeks.
02Approve on evidenceSomewhere anyone could look. Product, engineering and design could judge the new design on its merits, without the conversation turning into what Discourse would permit.
03Migrate by impactThe order comes from the audit, not from taste. Buttons first at 8.7% of all token use, then type, then spacing, then the one-off overrides.

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 · loading
01One canonical contractOne place that holds every agreed decision, belonging to no framework. Not Sass, not React, not Figma.
02Native implementationsReact and Ember each build the same thing their own way. They look identical without sharing a line of code.
03Generated adaptersCSS variables, Sass, Figma JSON. Every format is generated from that one place, never kept up to date by hand alongside it.

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.

909Tokens in the exportOne file, meant to feed both Figma and code.
14Top-level groupsRaw values, named values, components, and a bridge back to Discourse.
613Color tokensAlongside 149 sizes and 105 text values.
20Type stylesFont, weight, size, line height and letter spacing, all settled.
PhaseWhy it sits hereStatus
1 · ButtonsThe most used, at 8.7% of all token use. Not the most seen. See Two rankings.Roadmapped
2 · TypographyEverything after it sits on top of type. The DM Sans files are already confirmed from source.Planned
3 · Spacing & layoutGrid, 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.

Locating a decision

Before

Search 21 files, guess which components the change reaches, ship it and find out.

After

A ranked list of 105 components, with the file and line for every single use.

Shipping a visual change

Before

The change reaches some components and not others. Discovered in QA, or after launch.

After

Components read one named layer, so a color change spreads for one reason instead of four.

Design against production

Before

Figma and production drift apart until something ships wrong.

After

The audit draws the line between what is proven and what is not. Differences get written down, not discovered late.

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.

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

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.