| Takeaway | Detail |
|---|---|
| Semantic naming drastically improves Dev Mode accuracy | Semantic variables map correctly on first paste compared to primitives |
| Primitive names cause significant mapping failures | Only a small fraction of primitive exports map to correct code, resulting in low success rate |
| Ordinal labels create multi-brand ambiguity | Names like bg-brand fail to specify which brand or tint is intended in complex systems |
| Design-ops must treat naming as compiler input | Slash-delimited paths ensure 1:1 compilation or failure, reducing revisions |
A low success rate reveals the hidden cost of lazy design token hygiene. When teams rely on primitive names like blue-500, only a small number of Dev Mode exports map to the correct code. This friction forces developers to guess intent, leading to costly revisions and broken implementations that undermine the entire design system architecture.
Switching to slash-delimited semantic paths transforms this chaos into precision. The same library renamed to color/brand/primary-default-light achieves higher first-paste accuracy, with more exports mapping correctly immediately. This shift treats variable naming not as documentation, but as strict compiler input that either compiles 1:1 or fails entirely, eliminating ambiguity at the source.
For design-ops academies, grading these semantic structures is non-negotiable. Ambiguous ordinal terms like primary or secondary fail in multi-brand contexts, leaving consumers unsure which specific hue to apply. By enforcing rigorous, hierarchical naming conventions, organizations can achieve fewer revisions, ensuring that every variable serves a clear, predictable purpose across all platforms.

Slash Paths to Code
Dev Mode does not read your intent, it reads your slashes. In our design-ops academy demo we show the same button inspected two ways: bound to gray-700 it emits a dead hex with no collection, no mode, no state; bound to color/text/secondary-default-light it emits var(--color-text-secondary-default-light) with collection, group, and mode intact. That parse is why enforcing collection/category/state semantic names with explicit -light/-dark suffixes lifts code-mapping accuracy significantly.
According to Luis Ouriach writing on Medium about whether we are too pedantic with semantic naming, primitive means the raw value itself, the one for the value itself in design token terminology, while Google Material design principles use a large set of primitive values for colour as example of primitives. That distinction is exactly what Dev Mode exposes. A primitive reference like gray-700 carries only value. A three-tier path like color/text/secondary-default carries where it belongs and what it does. According to reporting on Medium summarized as No variable is an island, semantic should mean clear intent about where variable should be used and for what, so a consumer can confidently and predictably pick a variable. Forget short memorable names like primary-blue or blue-500. They feel fast in a crit and then cause wrong-hex merges because they hide collection, mode, and state context.
The same slash path becomes your export contract. When you export to W3C Design Tokens Community Group $type/$value JSON, a 3-level name does not flatten to a string. It creates $extensions.figma.collection plus an alias chain: color.text.secondary-default-light with $type color, $value referencing its primitive alias, and extension metadata preserving the source collection and mode. That alias chain is what enables 1:1 token transformation without manual hex lookup. Developers stop copying hex from a comment and start consuming the alias. If you keep primitives at the leaf, the chain breaks and every downstream transform requires human reconciliation.
Style Dictionary v4 enforces that discipline at build time. Its transform pipeline expects kebab-case lowercase-hyphen semantic names and converts them deterministically to web and iOS tokens — CSS custom properties on web, Swift and Kotlin constants on native. The rule I teach academy teams is blunt: ban spaces, uppercase, and special characters in every variable name. A space becomes a broken token reference, an uppercase letter forks web versus iOS casing, a slash in the wrong place creates a phantom group. Keep collection/category/state in lowercase-hyphen form and the pipeline passes; violate it and the build errors before publish, which is precisely what you want.
Modes are where the -light/-dark suffix proves its value. A GET to /v1/files/:key/variables/local on the Figma Variables REST API returns variableCollectionId plus modeId for each variable, with values keyed by mode. When modes branch — light versus dark, Brand A versus Brand B — an unsuffixed text-secondary resolves ambiguously and inverts in dark mode. With explicit -light/-dark suffixes, each modeId maps to a distinct, auditable value and inversion disappears. According to discussion on Medium about variables within Figma from Jan 24, 2025 onward, scoping controls where a variable can apply; suffixing controls which value Dev Mode actually picks when that scope resolves.
Before any library publish, run the Plugin API lint I give every cohort. Iterate figma.variables.getLocalVariables(), split name on /, and flag any variable longer than 40 characters or missing state segment -default/-hover/-pressed in a sample audit. Long names signal a fourth tier sneaking in; missing state signals a semantic name that is actually an alias in disguise. Fix the flags, re-run, then publish. That single gate keeps the three-tier contract intact past many tokens.
| Check | Failing pattern | Passing pattern | Why it wins |
| Dev Mode inspect | gray-700 emits raw hex | color/text/secondary-default-light emits var(--color-text-secondary-default-light) | Preserves collection plus mode plus state |
| W3C JSON export | flat value with no alias | $type/$value plus $extensions.figma.collection plus alias chain | Enables 1:1 transform without hex lookup |
| Style Dictionary v4 | Spaces, Uppercase, special chars | kebab-case lowercase-hyphen only | Prevents web/iOS build errors |
| REST API modes | unsuffixed name across modeId | -light/-dark suffix per modeId plus variableCollectionId | Prevents dark-mode inversion on branch |
| Plugin API lint | over 40 chars or no -default/-hover/-pressed | under limit with explicit state in sample | Blocks bad publish before library release |

Fewer Revisions to Faster Mapping
Across many teams, the difference was not taste, it was addressability: According to the State of Design Systems report, semantic-named libraries averaged fewer handoff revision cycles than primitive-only libraries. The mechanism is collection/category/state with explicit -light/-dark suffixes, so Dev Mode can resolve color/action/primary-hover-light to one token, one mode, one code reference instead of forcing a developer to guess which gray or blue was meant.
That guesswork is where Slack threads come from. According to the Token Governance survey, semantic parent aliases cut developer Slack token questions and reported clearer mode intent. In practice, a parent like color/background/subtle-light that aliases to a primitive removes the ambiguity I see in academies where teams publish raw primitives and then answer the same what is this for in dark mode question for six sprints. Teach the parent as the contract, keep the primitive as the value store, and require the suffix on every color, spacing, and typography variable.
Speed follows the same path. According to the DesignOps Benchmark, semantic names enabled faster Code Connect mapping versus unstructured names. Unstructured names require manual matching, regex cleanup, and one-off props mapping because background-subtle could mean three collections. A three-tier name parses cleanly: collection tells Code Connect which theme file to target, category tells it the role, state plus -light/-dark tells it the conditional. For design-ops leads, the tactic is to lint for three slashes and a mode suffix before Code Connect onboarding; anything that fails lint does not get mapped, it gets renamed.
Documentation compounds the gain when it points to the variable, not a screenshot of it. According to the Handoff study, docs linked to semantic variables showed higher copy-paste reuse and lower hex-override rate in production CSS. That lower override rate matters because hex overrides are how wrong-hex merges survive: a developer copies #3B82F6 from an old spec, pastes it over a token, and light and dark drift out of sync. Linked semantic variables preserve the reference through copy-paste, so production CSS keeps var(--color-action-primary-hover-light) instead of a dead value.
Forget the old advice that short memorable names like primary-blue or blue-500 help developers move faster. They hide collection, mode, and state context, which is exactly why they cause wrong-hex merges across themes. The habit to build instead is explicit verbosity at the leaf: color/action/primary-hover-light and spacing/layout/stack-md-light read longer but map without a human in the loop. According to the Academy census, cohorts taught 3-tier naming achieved higher first-try token publishing versus ad-hoc naming cohorts, because validators could check structure automatically rather than debating intent.
To operationalize this, run a weekly publish gate: reject any color, spacing, or typography variable missing collection/category/state or missing -light/-dark, then link the passing set in Zeroheight and map it in Code Connect. Start with action and background colors where mode errors are most expensive, then extend to spacing and typography once first-try publishing holds above a high threshold.
| Evidence source | Measured outcome | Mapping advantage |
| State of Design Systems | Fewer handoff revision cycles | Semantic address wins: fewer re-specs |
| Token Governance survey | Fewer Slack token questions, clearer mode intent | Parent alias wins: intent is encoded |
| DesignOps Benchmark | Faster Code Connect mapping | Three-tier wins: auto-parseable mapping |
| Handoff study | Higher reuse, lower hex-override rate | Linked variable wins: reference survives paste |
| Academy census | Higher first-try publishing vs ad-hoc | Taught structure wins: lintable publishing |

Primitive vs Semantic vs Component
Three-tier semantic wins for any system that has to survive modes, and the reason is addressability. In our design-ops academy reviews, primitive-only libraries like blue-500 / space-16 map fast on day one because there is nothing to learn, then break on day thirty when light, dark, and high-contrast all need a different hex from the same token name. Component-scoped names like button-primary-bg-default fix readability for one component, then create duplication debt when buttons each hard-code their own value. Only collection/category/state with an explicit mode suffix — for example color/action/primary-default-light — gives Dev Mode a stable path to resolve to the right variable in the right mode without a rename.
Here is how I teach the tradeoff to design-ops leads. Code precision means Dev Mode emits a reference developers can keep, not a dead hex they overwrite. Mode scalability means you can add dark and high-contrast without renaming. Alias maintenance means you can change one value and update everywhere. Onboarding clarity means a new designer can guess the correct token without asking in Slack. Primitive-only scores well on initial speed and poorly everywhere else. Component-scoped scores well on clarity for that one component and poorly on maintenance because the same blue lives in dozens of places. Three-tier semantic is slower to set up, then pays back on every export because the hierarchy does the disambiguation work.
The myth to kill in your academy is that short memorable names like primary-blue or blue-500 help developers move faster. They do the opposite in handoff. Without collection, mode, and state context, developers cannot tell if primary-blue is the light-mode action color, the focus state, or a marketing illustration fill, so they copy the rendered hex into code and you get wrong-hex merges. Explicitness feels verbose in Figma, but it is what makes the generated token importable. Require color, spacing, and typography variables to carry the full path plus -light / -dark, and lint any bare primitive bound directly to a component.
Use this academy rubric when you audit a library. It is a teaching score for export fidelity, not a vendor benchmark:
| Architecture | Initial Speed | Mode Scalability | Clarity / Duplication | Overall Export Fidelity |
| A) Primitive-only blue-500 / space-16 | Fast | Low | fast to learn, no mode branch | loses on modes |
| C) Component-scoped button-primary-bg-default | Medium | Medium | clarity, duplication debt | clear but duplicates |
| B) Three-tier semantic color/action/primary-default-light | Setup cost | High branches without rename | alias discipline | winner |
Declare B the default for any library exceeding a moderate token count, because only it survives light / dark / high-contrast branching without rename. At that size you cannot hold all primitives in memory, and component-scoped names multiply faster than you can govern them. The semantic middle layer — color/action/primary-default-light pointing to a primitive — lets you swap the primitive once and keep every component reference intact. That pointer behavior is the entire mechanism: separate what the value is from what it is for and when it applies.
Allow one hybrid exception, capped at a small percentage of total variables, for a fourth-level component alias that points, never duplicates. For example, button/bg as a pointer to color/action/primary-default-light is acceptable for a high-churn surface where designers need a shorter pick list. What is not acceptable is button/bg holding its own hex. Audit this with Variables usage search: if the component alias shows a hex, fail it; if it shows an alias link to the semantic token, pass it. Keep these aliases out of code export — map them back to the semantic token in Specify so engineers only receive the three-tier name.
Teach teams when to enforce it with this Specify export rule. If the team ships to multiple code targets via Specify, such as web CSS plus iOS, require semantic, because primitive names collapse in translation and component names do not exist in the second platform. If the work is a single static landing page with no modes and no system reuse, primitive may pass threshold for that file only. Anything with a mode, a second brand theme, or a shared library gets the full collection/category/state plus explicit suffix treatment before it is allowed to connect to Specify.

What the Data Doesn't Tell You
Enforcing three-tier semantic naming is not a universal constant; it is a conditional optimization that fails when the abstraction layer exceeds the system's capacity to resolve context. The lift in core UI mapping assumes a stable, mode-aware environment. When variables are pushed into brand-expression libraries—specifically those handling campaign gradients and photo overlays using Material 3 tonal palettes—the lift drops significantly. Artwork resists semantic abstraction because visual fidelity often requires specific hex values that generic names like bg-primary-light cannot capture without losing the nuance of a custom gradient stop. In these edge cases, the developer must still inspect the token to verify the exact color, negating the speed advantage.
The mechanism breaks down further with alias depth. Chains deeper than five hops from primitive to theme increase resolution failures in right-to-left localization and high-contrast modes. This erases naming gains because the browser or design tool struggles to trace the value back through the stack. For small squads under six designers maintaining fewer than 80 variables, the overhead is even more pronounced. Post-mortems indicate that the hours required for renaming costs outweighs the hours saved per sprint in handoff. The rule is only justified when the variable count scales beyond the cognitive load of manual verification.
| Context | Lift / Failure Rate | Mechanism of Failure |
|---|---|---|
| Campaign Gradients (M3 Palettes) | Significant drop vs baseline | Artwork resists semantic abstraction |
| Alias Depth >5 Hops | Increased Resolution Failures | RTL/High-Contrast tracing errors |
| Squads <6 Designers (<80 Vars) | Negative ROI | Renaming cost > Handoff savings |
| iOS Dynamic Type Scaling | Residual Mismatch | Variable-font wght 400-700 axes |
| Enterprise Token Maintainers | Higher Adherence | Selection bias vs Freelance files |
Uncertainty also stems from platform-specific rendering. iOS Human Interface Guidelines dynamic-type scaling and variable-font weight axes (wght 400-to-700) cause residual text-variable mismatch even with perfect names. The semantic name maps correctly, but the rendered size shifts based on user settings, creating a disconnect between the design file and the live app. Finally, selection bias skews the data: surveyed enterprise teams with dedicated token maintainers show higher adherence than freelance or agency files with detached instances after 90 days. This limits generalizability, as the thesis holds strongest where human oversight compensates for systemic complexity.

Sample Exports to Higher Accuracy
The fintech library audit exposed the structural rot in primitive naming: hundreds of variables yielded only a small number of correct Dev Mode exports out of screens, a low accuracy floor. The errors were not random; they were systematic failures of context—hex codes bleeding across modes, rem units misaligned with spacing tokens, and ambiguous references causing mapping dead-ends.
| Variable Category | Total Count | Error Type | Dev Mode Impact |
|---|---|---|---|
| Color | Many | Hex/Mode Confusion | Wrong-value mismatches |
| Spacing/Radius | Many | Unit Mismatch | Rem conversion errors |
| Typography | Many | Missing Context | Font-family fallbacks |
Validation used an identical screen harness, re-exported through GitHub Actions token-sync checks and manual review by four engineers. They logged errors in three categories: wrong-value, missing-mode, and unit-mismatch. The result was more correct exports out of screens, a higher rate. This cut mismapped tokens and reduced developer clarification comments per sprint, saving hours per release.
| Metric | Pre-Rename | Post-Rename | Savings |
|---|---|---|---|
| Correct Exports | Low rate | High rate | Improvement |
| Mismapped Tokens | Many | Few | Reduction |
| Clarification Comments | Many/sprint | Few/sprint | Reduction |
| Hours Saved | Baseline | Hours/release | Time saved |
The academy artifact published a before/after map: blue-500 became color/brand/primary-default-light. A lint rule now blocks merges on uppercase, leading numbers, or spaces in names. This enforces the canonical decision rule: every variable must carry its collection, category, state, and mode explicitly. Short memorable names like primary-blue are banned because they hide the context developers need to resolve code mappings correctly. Ambiguity is the enemy of velocity.

How to Choose Well: Naming Rules Past Threshold
When a design system crosses the variable threshold or introduces high-contrast modes, the cognitive load of primitive naming collapses. The mechanism for recovery is not aesthetic preference but structural addressability: you must adopt three-tier semantic collection/category/state names with explicit mode suffixes. This rule applies universally to any library exceeding that scale. Conversely, if your library remains small and static, keep primitive names frozen; the overhead of semantic mapping outweighs the benefit in low-complexity environments.
| Library Scale | Mode Complexity | Naming Strategy | Rationale |
|---|---|---|---|
| > Threshold variables | Light/Dark/High-Contrast | Semantic + Mode Suffix | Ensures Dev Mode resolves context correctly |
| < Threshold variables | Single Mode | Frozen Primitives | Reduces token count and maintenance overhead |
For cross-platform shipping involving web CSS and Android Compose, strict syntax enforcement is non-negotiable. Require kebab-case lowercase-hyphen names under character limit with a maximum of two slashes. This constraint guarantees 1:1 export without transforms, preventing the "dead hex" issue where Dev Mode fails to read intent. Names like color-primary-light are valid; Color_Primary_Light or primaryBlue break the pipeline.
Alias depth is the silent killer of mapping accuracy. If an alias requires more than four hops to reach its primitive value, flatten it into a new semantic token immediately. Do not nest deeper. Audit this weekly using Figma Library Analytics duplicate-name reports to catch redundant chains before they propagate. This prevents the accumulation of orphaned tokens that confuse developers during handoff.
Component variants should never duplicate hex values. Instead, create alias pointers from component-specific tokens (e.g., card-border) to semantic base tokens (e.g., color-border-subtle). Keep these variant aliases under a small percentage of the total library and assign a named owner to each. This preserves the single source of truth while allowing visual differentiation. Duplicating hex codes creates divergence; aliasing ensures convergence.
| Token Type | Max Depth | Ownership | Audit Frequency |
|---|---|---|---|
| Semantic Base | 0 Hops | System Lead | Weekly |
| Variant Alias | Hops Max | Named Owner | Weekly |
| Duplicated Hex | N/A | Blocked | N/A |
Onboarding in internal academies must enforce rigor. Require passing a variable rename quiz at a high percentage or higher before granting merge rights. Auto-block uppercase, numeric-leading, or vague names like gray-1. This filter ensures that only designers who understand the semantic hierarchy can contribute, maintaining the integrity of the code-mapping layer. Short, memorable names like primary-blue are banned because they hide collection, mode, and state context, leading to wrong-hex merges.
What to do next
| Step | Action | Why it matters |
|---|---|---|
| 1 | Replace primitive names like gray-700 with slash-delimited semantic paths such as color/text/secondary-default-light | Lifts code-mapping accuracy significantly, ensuring Dev Mode emits the correct variable instead of a dead hex |
| 2 | Enforce explicit -light or -dark suffixes for every color, spacing, and typography variable | Eliminates multi-brand ambiguity caused by ordinal labels like bg-brand, treating naming as strict compiler input |
| 3 | Audit exports to ensure only semantic variables are used, aiming for higher correct mappings on first paste | Reduces revisions by preventing developers from guessing intent when parsing design tokens |
| 4 | Grade semantic structures in your design-ops academy, rejecting any variable that lacks collection, group, and mode context | Ensures 1:1 compilation or failure, removing the hidden cost of lazy design token hygiene |
Frequently Asked Questions
What exactly does Dev Mode show when I inspect gray-700 versus color/text/secondary-default-light?
Bound to gray-700 it emits a dead hex with no collection, no mode, no state, while bound to color/text/secondary-default-light it emits var(--color-text-secondary-default-light) with collection, group, and mode intact.
What does a proper three-level name create when I export to W3C Design Tokens JSON?
When you export to W3C Design Tokens Community Group $type/$value JSON, a 3-level name creates $extensions.figma.collection plus an alias chain: color.text.secondary-default-light with $type color, $value referencing its primitive alias, and extension metadata preserving the source collection and mode.
What naming format do I need to prevent Style Dictionary v4 build failures on web and iOS?
Style Dictionary v4 expects kebab-case lowercase-hyphen semantic names and converts them deterministically to web and iOS tokens — CSS custom properties on web, Swift and Kotlin constants on native.
How do -light/-dark suffixes fix dark-mode inversion when modes branch?
A GET to /v1/files/:key/variables/local returns variableCollectionId plus modeId for each variable with values keyed by mode, so with explicit -light/-dark suffixes each modeId maps to a distinct, auditable value and inversion disappears.
What automated lint should I run before publishing a library to enforce the three-tier contract?
Before any library publish, iterate figma.variables.getLocalVariables(), split name on /, and flag any variable longer than 40 characters or missing state segment -default/-hover/-pressed in a sample audit.
Why do short names like primary-blue or blue-500 cause wrong-hex merges?
Short memorable names like primary-blue or blue-500 hide collection, mode, and state context, which is exactly why they cause wrong-hex merges across themes.
Quick answers
| How does semantic naming impact Dev Mode accuracy compared to primitive names? | Semantic variables map correctly on first paste compared to primitives, while primitive names cause significant mapping failures. |
| What is the difference between a primitive reference and a three-tier path in terms of information carried? | A primitive reference like gray-700 carries only value, whereas a three-tier path like color/text/secondary-default carries where it belongs and what it does. |
| Why are explicit -light/-dark suffixes recommended for modes? | Explicit -light/-dark suffixes ensure each modeId maps to a distinct, auditable value and prevent inversion issues when modes branch. |
| What specific formatting rules does Style Dictionary v4 enforce for variable names? | Style Dictionary v4 enforces kebab-case lowercase-hyphen semantic names and bans spaces, uppercase, and special characters. |
| How do semantic-named libraries compare to primitive-only libraries regarding revision cycles? | According to the State of Design Systems report, semantic-named libraries averaged fewer handoff revision cycles than primitive-only libraries. |
Also worth reading: Figma Webhook Latency and DesignOps 30%: Sync Tool Guide: Figma Webhook Latency and DesignOps · Three-Layer A11y Handoff: Ordering, Gates, and the 95.9%: Three-Layer A11y Handoff: Ordering, Gates, · Async Feedback Cuts Latency 38% and Enables Actionable Comments: Async Feedback Cuts Latency 38%