The Cosmic Ledger

Learn

Design System Drift: The Five Types and How to Detect Them

Design system drift is the gap between your design and code. Here are the five types, how to detect each, and why the oldest difference is the real finding.

Your product is quietly becoming a different product

Open your product next to your Figma file and look for longer than you usually do. The primary color on the checkout page is a shade off the one in the library. The form fields in settings sit 4px closer together than the ones in onboarding. The modal on the billing page has corners nobody designed. The Disabled state of your main button exists in Figma and has never been reviewed in code. Each difference is small enough to ignore. Together they mean your design system and your shipped product now describe two different products, and nobody decided that.

You have a design system. You may have a tokens pipeline, a Storybook, a handoff process and a design QA step. All of them can be working, and this can still be true, because none of them answers the one question that matters: do the design and the code still agree, right now, everywhere?

This post gives that problem a name and a method. By the end you will be able to take any difference between your design and your shipped UI, say which of five types of drift it is, pick the right way to detect it, and estimate how long it has been alive. That last part changes how you prioritize everything else.

What design system drift actually is

Design system drift is the measured gap between a design decision and its code reality. A token, a component, a variant or a state is defined one way in the design source and implemented another way in code, and nothing in your workflow reports the difference.

Every product team runs one design system in two places. The design side lives in Figma: components, variants, properties, styles, variables. The code side lives in the UI codebase: components, props, CSS, token files, Storybook stories. Each side has its own editors, its own history and its own release rhythm. Handoff connects them once. After that, they evolve in parallel, and parallel lines that nobody measures do not stay parallel.

Drift is what happens to two copies that nobody compares. A developer overrides a value locally because the pipeline was broken that week. A designer adjusts a variant after the component shipped. A token gets renamed on one side. A hotfix hardcodes a color to get a release out. None of these are mistakes. Each one is a reasonable change made by someone doing their job, which is exactly why drift is not a people problem. It is the default state of any system kept in two places without a comparison between them.

This is why "we have a design system" and "our product matches our design system" are different claims. The first is about what exists. The second is about whether the two copies still agree, and only a comparison can answer it. Keeping design-code sync is not a setup task you finish; it is a check you keep running.

Why drift costs more than it looks

A single difference is cheap. Drift is expensive because it compounds, and it compounds in four places at once.

It spreads. Every new screen is built from what already exists in code, not from what is in Figma. One drifted value becomes the reference for the next ten screens, and the gap between the product and the design widens without anyone touching the design system.

It turns reviews into arguments. When design and code disagree and nobody can say which changed or when, every design review becomes a debate over screenshots. Designers learn to stop raising small differences. Developers learn that the Figma file is a suggestion. The system loses authority one shrug at a time.

It hides inside green checks. Pipelines push values one way and report success. Tests confirm the code does what the code says. Neither checks that the result still matches the design, so every check stays green while the product moves.

It now moves at agent speed. AI coding agents build UI from whatever the codebase shows them. If the codebase says 8px where the system says 12px, the agent copies 8px, correctly and confidently, into every component it writes. Drift used to spread at the pace of a team. Now it spreads at the pace of generated code.

The five types of drift

Drift is not one thing. A color that changed and a state that was never built are different failures, found in different places, with different fixes. Five types cover what you will find when you compare any design component with its coded counterpart.

1. Structural drift

The component is built differently from how it is designed: an element is missing or extra, or the hierarchy differs. Example: the design's Card has a dedicated footer area for actions; the coded Card has none, so every team builds its own footer, and the product ends up with five versions of the same Card.

2. Property drift

The component's options do not match. A design property has no matching prop, a prop accepts values the design never defined, or the names diverged. Example: the design's Input has Size=Small | Medium | Large; the code's size prop accepts "sm" | "md". Large exists in Figma and nowhere else, so whoever needs it improvises one.

3. Token drift

A styling value no longer matches its design token, either because the token's value changed on one side or because code stopped using the token. Example: the brand's primary color is updated in Figma Variables; three components pick it up through the pipeline, two have it hardcoded and keep the old value. Your brand now ships in two shades, depending on which screen you are on.

4. Variant drift

A variant or state exists on one side and not the other, or exists on both but is never checked. Example: the design defines Hover, Focus, Pressed and Disabled for every Button type; Storybook has stories for the default state only. The other states ship, and nothing ever renders them next to their design. Focus states are where accessibility quietly erodes.

5. Visual drift

The structure, props and tokens all look right in the source, and the rendered result is still different. Spacing, line height and inherited styles combine into an output nobody wrote down. Example: the Modal's padding matches its token, but an inherited line height makes its header 4px taller than the design on every page that uses it. No single value is wrong. The result is.

The order is deliberate. The first four types can often be found by reading values. The fifth only shows up when you look at what actually renders, which is why any review of the source alone will always miss some drift.

How to detect each type

This is the working center of the post: for each type, what to compare and what signal tells you that you found it. Take one component you suspect, and run it through all five rows. To keep it concrete, the last column follows a single Button through every row.

The Button values below (a 12px radius in design and 8px in code, a hardcoded hover color, a missing state) are illustrative, built from the demo data we use to show the product. The method is real and works on your own system.

TypeCompareSignal that you found itButton example
StructuralThe design component's layer tree vs the code component's rendered elementsAn element exists on one side only, or sits at a different levelIcon slot in design, no icon element in code
PropertyFigma component properties and their values vs the code component's props and accepted valuesA property with no prop, a value with no counterpart, a name mismatchSize=Large in design, size accepts only "sm" and "md"
TokenThe token each design value is bound to vs the value and token reference in codeA different value, or a raw value where a token reference should beradius/md (12px) vs 8px; action/hover vs a hardcoded #7a4dff
VariantThe full list of design variants and states vs the list of code variants that are actually rendered somewhere (stories, tests, examples)A variant that exists in design and is never rendered from codeState=Disabled has no story
VisualThe rendered component in each state vs the design frame for the same state, at the same sizeA visible difference that no single source value explainsRendered height 4px taller than the design

How to run each comparison by hand, this week:

Structural. Open the component in Figma and expand its layers. In the browser, open the component (a Storybook story is ideal) and inspect the element tree. Write the two hierarchies side by side. Ignore wrapper divs that exist only for layout.

Property. List the component properties in Figma's right panel: every property and every value. Then open the component's type definition or prop types in code. Two lists, line by line. Names that differ only in casing or abbreviation (Large vs lg) are fine if a mapping exists somewhere. If no mapping exists, it is drift.

Token. For each styled property (radius, color, spacing, typography), note the variable or style the design binds it to and its resolved value. Then check what code actually applies. In the browser, getComputedStyle gives you the final value:

1getComputedStyle(document.querySelector('button.primary')).borderRadius

Compare the resolved values, then check the source: a matching value that is hardcoded instead of referencing the token is still drift. It matches today and will diverge the next time the token changes.

Variant. Make two lists: every variant and state combination in the Figma component set, and every combination your code renders somewhere you can look at it. In most teams that second list is your Storybook. Every design variant with no story is a variant nobody compares. Hover, focus, pressed and disabled are the usual gaps.

Visual. Render the coded component in each state at the design's size, and put it next to the design frame. Look for differences in size, spacing and alignment first; they are easier to see than color shifts. When you find one, go back up the table: most visual drift has a cause in one of the four rows above, and the visual row is how you discover that the cause exists.

Done by hand, this takes a few minutes per component. That is enough to classify any difference you can see in your product this week. It is not enough to keep a whole system of components and their variants compared after every change, which is the honest limit we come back to at the end.

How long has it been drifting?

Classifying drift tells you what is wrong. The age of a difference tells you what it has cost. A value that changed yesterday is a typo. A value that has been wrong for seven months has been copied into new screens, matched by new components, screenshotted into documentation, learned by your agents and treated as correct by everyone who joined in that time. The oldest difference you find is usually the real finding, because it has had the most time to spread.

You can estimate age from both sides' history.

On the code side, git can find when a value appeared. The pickaxe option searches for the commits that introduced or removed a string:

1git log -S "8px" --oneline -- src/components/Button/

Run it against the value you found in code. The oldest commit in the output is when the code side took its current shape.

On the design side, Figma's version history shows when the component last changed. Find the version where the value took its current form.

The difference's age is the time since the two sides last agreed: from the later of those two changes, to today. If the code changed in March and the design has said otherwise since January, the drift started in March, and every screen built since March was built against the wrong reference.

Two habits make this useful rather than academic:

  1. Record the age next to every difference you log. "Radius 12px vs 8px" is a ticket. "Radius 12px vs 8px, drifting since the March commit" is a priority.
  2. Sort by age, not by size. A 1px difference that has lived for a year has usually spread further than a 4px one from last week.

Where to start on Monday

Pick the component your product leans on most, usually Button, Input or Card. Run it through the five rows above, one variant at a time, and log each difference with three things: its type, both values, and its age. Then share the list with design and code owners together, sorted oldest first.

The point of this first pass is not a clean system. It is a shared, dated list that both sides agree describes reality. Most teams have never had one. Once you do, the conversation changes from "it looks off" to "this is token drift, 12px against 8px, since March", and that is a conversation a team can actually finish.

What detection does not do

Detection names drift and dates it. It does not close it. Deciding which side is right, and getting the other side to match, is a workflow of its own, with design and code owners both in the room. A detected difference can also be intentional: code sometimes moves ahead of the design on purpose, and the right fix is to update Figma, not the component.

This method also has a boundary. It compares what is defined with what is built. It will not catch a broken interaction or a logic bug, which are failures of behavior, not drift between two descriptions of the same component. And the manual version stops scaling quickly: it is a reading of one afternoon, and the system keeps changing the day after.

That is the gap a continuous comparison closes. deeepSpace renders each coded component from Storybook, compares every variant against its Figma original with matching props, checks tokens rather than just pixels, and traces each difference to the commit that introduced it. If you want to see what that looks like on a real comparison, read how design drift detection works. And if you want to run it on your own system, Crew starts with a 7-day free trial, no credit card required.

Alignment is not a design system's resting state. Without something comparing the two copies, small changes accumulate the way objects fall: no one pushes them, they just go where gravity takes them. Detection is the counterforce, and it starts with knowing which of the five types you are looking at.