Linting Your Design Tokens - The What And The Why
Design tokens are code: the single source of truth for design decisions — colour, typography, spacing, shadows — across your design system. When they're right, they keep products consistent. When they're wrong, the impact spreads quickly.
Consider a few common failures:
- A designer adds a new brand colour in Figma and syncs it to the repo, not
realising that
color.brand.primarynow referencescolor.brand.accent, which already referencescolor.brand.primary. - A developer renames
spacing.component.buttontospacing.interactive.buttonduring a refactor and misses three references buried in component tokens. - Someone pairs
color.text.mutedwithcolor.surface.subtlewithout checking contrast, and it ships to production with a failing ratio. - A token called
color.feedback.errorDarkappears alongsidecolor.status.error-dark, and six months later nobody knows which one the mobile team is using.
These problems might not show up in design tools. They won't necessarily trigger transform errors. They simply, silently slip into your codebase, fragmenting your design system and creating technical debt.
Design tokens need linting, the same way CSS and JavaScript do. Not as a nice-to-have, but as a core part of your workflow.
The Case for Linting Design Tokens
Design Tokens Are Under Protected
Consider this workflow without linting:
Designer exports tokens from Figma → Developer imports into codebase → Tokens transform into CSS/Swift/etc → Webste/App ships
If there's a problem in the tokens, it may only appear when someone uses them, or worse, when users encounter broken styling in production.
With linting integrated at the right points in that workflow, problems get caught before they travel any further at a moment that they're cheapest to fix.
Problems Design Token Linting Prevents
The Silent Duplicate
{
"spacing": {
"medium": { "value": "16" },
"md": { "value": "16" }
}
}Both get shipped. Teams argue about which to use, confusion propagates, and you end up with the kind of naming inconsistency that makes maintenance a nightmare.
The Broken Reference
{
"colors": {
"semantic": {
"success": { "value": "{colors.primary.dark}" } // Doesn't exist
}
}
}Transformation fails, developers spend 30 minutes debugging a reference that should never have been broken, and the PR sits blocked while someone traces it back to a rename that happened two weeks ago.
The Accessibility Regression
{
"colors": {
"background": { "value": "#FFFFFF" },
"textMuted": { "value": "#FAFAFA" } // 1.08:1 contrast. Fails WCAG AA.
}
}It ships to production, survives until an accessibility audit catches it, and then becomes an expensive retrofit, the kind that damages both the product and confidence in the design system.
The Naming Inconsistency
{
"colors": {
"primary_color": {}, // snake_case
"secondaryColor": {}, // camelCase
"tertiary-color": {} // kebab-case
}
}New developers don't know the convention, so they pick one. Inconsistency compounds, tooling starts to break, and onboarding gets harder with every new addition to the system.
Why It Matters Across the Team
Linting helps everyone involved in the workflow. Designers get faster feedback before handoff, developers catch issues before review, and maintainers keep the system consistent as it grows.
Product teams benefit too: fewer regressions, fewer accessibility misses, and less avoidable debt.
What Should You Lint?
Design token linting rules can fall into three categories.
Critical rules: prevent system breakage, requiring violations must be fixed.
Quality rules: enforce consistency and best practices, producing warnings to address.
Nice-to-have rules: help teams work better together, improving team cohesion. Suggestions to consider rather than requirements to meet.
The JSON examples in this article use the format most common in popular tooling — value, type, description without a $ prefix. The W3C DTCG spec uses $value, $type, and $description. The linting concepts are the same regardless of which format your toolchain expects; the properties you check just differ by prefix.
Critical Rules
These rules prevent your design system and end products from breaking. If any of them fire, they should block commits and deployments.
No Broken References
Every reference to another token must point to a path that actually exists.
{
"colors": {
"semantic": {
"primary": { "value": "{colors.base.blue}" } // ← Doesn't exist
}
}
}Broken references break token transformation to all platforms, meaning deployment failures and runtime errors. The most common causes are a rename that leaves stale references elsewhere, and copy-paste typos in reference paths, both of which a linter catches before anything reaches the end user.
No Circular References
Tokens must not reference themselves, whether directly or through a chain.
{
"colors": {
"primary": { "value": "{colors.secondary}" },
"secondary": { "value": "{colors.primary}" } // ← Circular!
}
}Circular references create infinite loops during transformation, breaking every platform's output format: CSS, Swift, Kotlin, all of them. They also tend to indicate a deeper architecture problem. Common causes are refactoring that creates unexpected dependencies, or a reference chain that loops back on itself.
Type-Value Matching
Token values must match their declared type and be in the correct format. Mismatches come in several forms:
{
"spacing": {
"md": {
"value": 16, // ← Unitless number. Dimensions must be strings like "16px"
"type": "dimension"
}
},
"colors": {
"primary": {
"value": "blue", // ← Not a valid colour format
"type": "color"
}
},
"opacity": {
"disabled": {
"value": "1.5", // ← Opacity can't exceed 1
"type": "opacity"
}
}
}Type-specific checks:
- Colours: Valid hex (
#007AFF), rgb, or hsl. Not arbitrary strings or named colours. - Dimensions/Spacing: String values with the unit included (
"16px","1rem"). A unitless number is invalid. - Typography: Appropriate values (fontSize as number, fontWeight as number, lineHeight as ratio)
- Opacity: Values between 0 and 1
- Border radius: Numeric values, not negative
- Dimensions: No negative values for sizes or spacing
Mismatched types can prevent transformation tools from doing their job, because they won't be able to transform what they can't understand. Keeping type and value aligned makes platform expectations explicit and catches typos and formatting mistakes before they travel downstream.
Complete Token Structure
In practice, every token needs a well-defined value and a resolved type. The type can be defined directly on the token or inherited from a parent group, but transformation tools need to be able to determine it — if they can't, debugging becomes a guessing game.
{
"colors": {
"primary": { "value": "#007AFF" } // ← Missing type property
},
"spacing": {
"md": {
"value": "16px"
// ← Missing type property
}
}
}The complete form per the DTCG spec:
{
"colors": {
"primary": {
"value": "#007AFF",
"type": "color"
}
},
"spacing": {
"md": {
"value": "16px",
"type": "dimension"
}
}
}Incomplete metadata causes transformation failures and makes debugging painful. Enforcing whatever structure your toolchain requires keeps the system consistent and predictable across every token in the set.
Quality Rules
These rules enforce consistency and prevent confusion. When they trigger, they should surface as warnings for the team to address rather than hard blocks.
Naming Convention
All token names should follow a single consistent pattern: camelCase, kebab-case, or whatever your team has agreed on. Mixing conventions in the same system is going to cause real problems:
{
"colors": {
"primary": {},
"Primary": {}, // ← Capital P breaks convention
"primary_light": {}, // ← snake_case breaks convention
"secondary-dark": {} // ← kebab-case breaks convention
}
}Consistent naming makes tokens predictable and discoverable. Automation tools depend on it, and inconsistency will add cognitive load for everyone working with the system, especially newcomers who don't know which convention to reach for.
No Duplicate Values
If two tokens share the same value, one should reference the other rather than repeat it.
{
"spacing": {
"small": { "value": "8px", "type": "dimension" },
"xs": { "value": "8px", "type": "dimension" } // ← Duplicate value
}
}Consolidating through references means you update one place and every reference follows:
{
"spacing": {
"xs": { "value": "8px", "type": "dimension" },
"small": { "value": "{spacing.xs}", "type": "dimension" }
}
}Duplicate values can create bloat and confusion about which token to use. They may also reveal when someone has misunderstood the system's structure, which makes it especially worth enforcing for colours, spacing, and typography.
Required Tokens Present
Your token system chould include essential tokens for every platform. A set that
only defines primary but skips error, success, and warning may be incomplete:
{
"colors": {
"primary": { "value": "#007AFF" }
// ← Missing error, success, warning states
}
}A more complete set:
{
"colors": {
"primary": { "value": "#007AFF" },
"error": { "value": "#FF3B30" },
"success": { "value": "#34C759" },
"warning": { "value": "#FF9500" }
}
}Incomplete sets could lead to partial implementations that will feel inconsistent across platforms. This catches the common case where someone adds a colour but forgets the full set of semantic states that go with it.
Colour Contrast Compliance
Colour token values should meet WCAG accessibility standards, but there's a catch: a linter can only check contrast between two tokens if it knows they'll be used together. That relationship isn't inherent in the tokens themselves at the primitive or semantic level.
The most reliable place to enforce contrast is at the component token level, where the pairing is explicit:
{
"button": {
"background": { "value": "{colors.primary}", "type": "color" },
"label": { "value": "{colors.white}", "type": "color" }
// ← A linter can check these two against each other with confidence
}
}At the semantic level, you can use a naming convention to imply the relationship — an on-primary token is understood to sit on top of primary — which gives a linter enough context to check the pair:
{
"colors": {
"primary": { "value": "#007AFF", "type": "color" },
"onPrimary": { "value": "#FFFFFF", "type": "color" }
}
}WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. AAA raises that to 7:1 and 4.5:1 respectively.
Contrast failures are one of the most common accessibility issues. The earlier they're caught in the token hierarchy, the less likely they are to reach components or users.
Dark Mode Completeness
Every light mode token should have a corresponding dark mode variant. A token without one leaves dark mode partially broken:
{
"colors": {
"background": { "value": "#FFFFFF" }
// ← Missing dark variant
}
}A complete pairing looks like:
{
"colors": {
"background": { "value": "#FFFFFF" },
"backgroundDark": { "value": "#1A1A1A" },
"text": { "value": "#000000" },
"textDark": { "value": "#FFFFFF" }
}
}This example uses a naming suffix convention. Your system may use separate theme files, modes, or different naming patterns. The principle remains: ensure every visual token has a dark mode equivalent.
This catches cases where designers add a light mode token but forget the dark mode equivalent, keeping both themes in sync and ensuring every user preference is respected.
Reference Depth Limits
Token references shouldn't chain through too many layers. Following a five-hop chain like this can be a painful nightmare for both people and tools:
{
"colors": {
"interactive": { "value": "{colors.brand.primary}" },
"brand": {
"primary": { "value": "{colors.palette.blue}" }
},
"palette": {
"blue": { "value": "{colors.system.blue}" }
},
"system": {
"blue": { "value": "{colors.base.blue}" }
},
"base": {
"blue": { "value": "#007AFF" }
}
}
}A shallow reference is much easier to follow:
{
"colors": {
"base": {
"blue": { "value": "#007AFF" }
},
"primary": { "value": "{colors.base.blue}" }
}
}When something breaks in a deep chain, debugging can become slow and painful. Keeping references shallow encourages a clean and intentional hierarchy and makes it possible to follow a token's value without opening multiple files.
Nice-to-Have Rules
These rules help teams work better together and maintain long-term health. They're suggestions, not requirements.
Token Documentation
Tokens should include descriptions that explain their purpose and intended use, not just their value.
{
"colors": {
"primary": {
"base": {
"value": "#007AFF",
"type": "color",
"$description": "Primary brand color used for CTAs, links, and key interactions. Do not use for backgrounds."
},
"light": {
"value": "#E8F4FF",
"type": "color",
"$description": "Light tint of primary, used for hover states and backgrounds. Pairs with primary.base for sufficient contrast."
}
}
}
}Without descriptions, tokens are just values. With them, they become design guidance that explains intent, prevents misuse, and documents the decisions behind each choice. Descriptions are also machine-readable: documentation sites can pull them in automatically, design tools like Figma can surface them as tooltips, and code editors can show them as hover text. Worth adding when there's any ambiguity about when to use one token over another, or when new team members regularly have to ask.
Semantic Naming
Token names should describe intent, not appearance — but this depends on which level of your token hierarchy you're at.
At the primitive level, appearance-based names are correct. blue.500 is exactly the right name for a raw colour value: it describes what it is.
At the semantic and component levels, appearance-based names become a liability. If your semantic tokens look like this, you have a problem:
{
"colors": {
"semantic": {
"lightBlue": { "value": "{colors.base.blue.200}" },
"darkGray": { "value": "{colors.base.gray.800}" }
}
}
}Semantic names survive design changes:
{
"colors": {
"semantic": {
"primary": {
"base": { "value": "{colors.base.blue.500}" },
"subtle": { "value": "{colors.base.blue.200}" }
},
"text": {
"secondary": { "value": "{colors.base.gray.800}" }
}
}
}
}If the brand moves from blue to teal, semantic.lightBlue is now wrong in both name and implication. semantic.primary.subtle doesn't care what colour it resolves to. Worth flagging when semantic or component tokens are named after colours, tints, or hex values, or when a rebrand would require renaming rather than just changing values at the primitive level.
No Unused Tokens
Tokens defined in the system should actually be referenced somewhere.
{
"colors": {
"legacy": {
"blue": { "value": "#007AFF" } // ← Never used anywhere
}
}
}Unused tokens add noise without adding value. They make the system harder to understand, tempt developers to reach for the wrong thing, and inflate output file sizes. Worth flagging when tokens haven't been referenced in recent history or when design changes have made old tokens irrelevant.
This is complex to track automatically (it would need access to consuming code), but useful as a periodic audit. It's also worth scoping this rule to semantic and component tokens only — primitive tokens may form a complete scale that's only partially referenced, or be generated automatically from tooling, so flagging them as "unused" would produce a lot of noise without much signal.
Consistent Value Formats
Similar tokens should use consistent value representations. Mixing formats creates friction:
{
"colors": {
"primary": { "value": "#007AFF" }, // hex
"secondary": { "value": "rgb(0, 81, 213)" }, // rgb
"tertiary": { "value": "hsl(217, 100%, 42%)" } // hsl
}
}Pick one and stick to it:
{
"colors": {
"primary": { "value": "#007AFF" },
"secondary": { "value": "#0051D5" },
"tertiary": { "value": "#003FA3" }
}
}Tools have to handle every format you use, and contributors don't know which to reach for when adding new tokens. A single format keeps the system easy to read and process.
Token Naming Hierarchy Alignment
The cleanest solution to token hierarchy is structural: keep primitives, semantic tokens, and component tokens in separate files. When each tier lives in its own file, there's no ambiguity about what belongs where. This also makes it much easier to enforce tier-specific rules — like semantic naming only applying to semantic and component files, or unused token checks skipping primitives entirely.
If everything lives in a single file, naming has to carry that weight instead, and it's a harder problem to solve with naming alone. A flat mix of raw colours, semantic names, and grouped semantic names is confusing:
{
"colors": {
"blue": {},
"primary": {},
"semantic": {
"success": {}
}
}
}Grouping by tier within the file at least makes the structure navigable:
{
"colors": {
"base": {
"blue": {},
"green": {}
},
"semantic": {
"primary": {},
"success": {}
}
}
}This rule is a signal to consider whether your token architecture would benefit from separate files per tier. When names are doing the job that structure should be doing, it's worth revisiting the organisation rather than just enforcing a naming pattern.
Size/Scale Consistency
Tokens that represent scales tend to follow a logical progression. Irregular jumps could make spacing feel arbitrary:
{
"spacing": {
"xs": { "value": "4px", "type": "dimension" },
"sm": { "value": "8px", "type": "dimension" },
"md": { "value": "20px", "type": "dimension" }, // ← Inconsistent jump
"lg": { "value": "28px", "type": "dimension" } // ← Another irregular jump
}
}A consistent multiplier makes the scale predictable:
{
"spacing": {
"xs": { "value": "4px", "type": "dimension" }, // 4
"sm": { "value": "8px", "type": "dimension" }, // 4 × 2
"md": { "value": "16px", "type": "dimension" }, // 8 × 2
"lg": { "value": "32px", "type": "dimension" }, // 16 × 2
"xl": { "value": "64px", "type": "dimension" } // 32 × 2
}
}Consistent scales can create visual harmony and reduce guesswork. When spacing or font sizes follow a predictable pattern, implementation may be faster and the system will feel intentional rather than assembled piece by piece.
Platform Coverage
Tokens should exist for all your target platforms, not just the primary one.
{
"colors": {
"primary": { "value": "#007AFF", "type": "color" }
},
"spacing": {
"md": { "value": "16px", "type": "dimension" }
}
// ← Only web tokens, no mobile equivalents
}When one platform gets a full token set and another doesn't, the gap shows in the product. Platform coverage rules make it explicit when a token is platform-specific rather than universal, and flag when new platforms join without getting the full set.
Metadata Consistency
Tokens should include consistent metadata fields beyond the core value and type: tags, aliases, and cross-references.
{
"colors": {
"primary": {
"value": "#007AFF",
"type": "color",
"$description": "Brand primary color",
"$extensions": {
"com.your-org.design-system": {
"tags": ["brand", "interactive", "semantic"],
"references": "Brand Guidelines v2.1"
}
}
}
}
}Rich metadata helps turn a token file into living documentation. Tags and references make tokens searchable, surface relationships, and help teams understand why decisions were made, with automation like "show me all brand tokens" possible as a bonus.
How Severity Levels Work
Not all linting issues are equal. A broken reference is catastrophic. A missing dark mode variant is important but may not be urgent. Most linting tools map this to three levels: errors that block progress until fixed, warnings that surface problems without stopping work, and informational suggestions that nudge without demanding anything.
Error
Errors block commits, PRs, and deploys. They represent problems that prevent the system from working at all — there's no safe way to proceed until they're resolved.
- Broken references
- Circular dependencies
- Type-value mismatches
- Incomplete token structure
- Accessibility failures
Warning
Warnings are reported but don't block. They flag issues that should be addressed, but won't immediately break anything — the kind of thing you fix in the next sprint, not right now.
- Naming inconsistencies
- Duplicate values
- Unused tokens
- Missing dark mode variants
- Insufficient documentation
Info/Suggestion
Suggestions are helpful guidance with no pressure attached. They point toward better practices without demanding them, useful for teams still finding their footing with a rule or working incrementally toward a standard.
- Nice-to-have rules
- Best practice recommendations
- "Consider documenting this token"
- "This could be simplified"
Configuring Rules by Need
Not every team needs every rule. Here's how to think about building up your ruleset:
Minimum viable linting (start here)
- No broken references
- No circular dependencies
- Naming convention
- Type-value matching
- This catches 80% of real problems with minimal overhead
Standard linting (most teams)
- All critical rules
- Duplicate detection
- Accessibility compliance
- Required tokens
- This ensures completeness and quality
Comprehensive linting (mature systems)
- All previous rules
- Dark mode pairing
- Documentation requirements
- Semantic naming enforcement
- This optimizes for team productivity and long-term health
Your Tokens Are Code
Design token linting is not about restricting your team. It's about protecting your design system from silent (and not so silent) failures.
Without it, problems will hide until they affect users. Inconsistencies will accumulate, refactoring becomes risky, and designers and developers lose confidence in the system.
The rules in this article cover a full spectrum, from hard blocks on broken references and circular dependencies, through quality checks on naming and duplicate values, to softer suggestions on documentation and hierarchy. Not every team needs every rule. Start with the critical ones. Add others as your team grows into them.
Your design tokens are code. They deserve the same rigor. Lint them.