CSS Variables Naming in Design Systems: The Right Way to Scale

I share the exact CSS variable naming conventions I use in large-scale design systems to keep tokens consistent, maintainable, and team-proof across every component.


Three years ago, I inherited a design system with over 400 CSS custom properties named things like --blue, --blue2, --blueNew, and — my personal favorite — --blueActualFinal. Changing the brand’s primary color took me two full days and introduced three regressions. That experience permanently changed how I think about CSS variables naming conventions. If you’re building or maintaining a large-scale design system, the names you choose for your custom properties are not a cosmetic concern — they are your architecture. Get them wrong, and every refactor becomes a nightmare.


TL;DR

  • Use a three-tier token hierarchy: global → semantic → component-level tokens.
  • Follow a consistent --[namespace]-[category]-[variant]-[state] pattern for every variable.
  • Semantic tokens (e.g., --color-text-primary) are what components should consume — never raw global values like --color-blue-500.

Why CSS Variable Naming Conventions Matter in Design Systems

Design tokens are the atoms of a design system — colors, spacing, typography, and more, stored as named values. When those names are inconsistent, the system becomes unmaintainable at scale. In my experience, teams with more than three engineers working on the same codebase hit this wall fast.

The CSS custom properties spec gives us enormous flexibility [SOURCE: https://www.w3.org/TR/css-variables-1/], but flexibility without conventions leads to chaos. A clear naming strategy makes your tokens self-documenting, tool-compatible (with Figma Tokens, Style Dictionary, or Theo), and resilient to redesigns.

Poor naming also breaks design-developer handoffs. When Figma uses one naming model and the codebase uses another, errors creep in silently. Aligning both sides on the same convention eliminates an entire class of bugs.


Prerequisites

Before diving in, you should be comfortable with:

  • CSS custom properties (--my-var: value; color: var(--my-var);)
  • Basic design system concepts (tokens, components, themes)
  • Optionally: a token transformation tool like Style Dictionary [SOURCE: https://amzn.github.io/style-dictionary/]

These practices apply whether you’re writing raw CSS, using Sass, or managing tokens via JSON fed through a build pipeline.


Step-by-Step: My Naming System for CSS Variables at Scale

Step 1 — Establish a Three-Tier Token Architecture

The single biggest improvement I made was separating tokens into three tiers:

  1. Global tokens — raw design values, never used directly in components.
  2. Semantic tokens — meaningful aliases that describe usage, not appearance.
  3. Component tokens — scoped overrides for specific UI elements.
/* Tier 1: Global */
--color-blue-500: #3B82F6;
--color-blue-600: #2563EB;
--space-4: 1rem;

/* Tier 2: Semantic */
--color-action-primary: var(--color-blue-500);
--color-action-primary-hover: var(--color-blue-600);
--space-component-gap: var(--space-4);

/* Tier 3: Component */
--button-bg: var(--color-action-primary);
--button-bg-hover: var(--color-action-primary-hover);

Components only consume Tier 2 or Tier 3 tokens — never Tier 1. This means when the brand color changes, you update one global token, and everything cascades correctly.


Step 2 — Define a Strict Naming Pattern

Every token I create follows this pattern:

--[namespace]-[category]-[property]-[variant]-[state]
SegmentExample ValuesRequired?
namespaceds, brand, appYes
categorycolor, space, font, borderYes
propertybg, text, radius, sizeYes
variantprimary, subtle, inverseOptional
statehover, focus, disabled, activeOptional

A real example from a production system I maintain:

--ds-color-text-primary: #111827;
--ds-color-text-primary-disabled: #9CA3AF;
--ds-color-bg-surface-raised: #FFFFFF;
--ds-space-layout-section: 2.5rem;
--ds-font-size-body-md: 1rem;
--ds-border-radius-card: 0.5rem;

The namespace prefix (ds-) prevents collisions with third-party libraries and makes every token immediately identifiable in DevTools.


Step 3 — Separate Color by Role, Not by Hue

One of the most common mistakes I see is naming tokens by their visual value: --color-blue, --color-gray-dark. This breaks the moment a theme changes or dark mode is added.

Instead, name by role:

/* ❌ Fragile — names the color */
--color-blue: #3B82F6;
--color-gray-dark: #374151;

/* ✅ Resilient — names the role */
--ds-color-text-default: #374151;
--ds-color-text-link: #3B82F6;
--ds-color-text-link-visited: #7C3AED;

When I migrated a client’s design system from light-only to supporting dark mode, this structure reduced the migration from an estimated two weeks to three days. Theme switching just required swapping semantic token values — component code stayed untouched.


Step 4 — Namespace Component Tokens with the Component Name

For component-specific tokens, prefix with the component name before the property:

/* Button component tokens */
--ds-button-color-bg: var(--ds-color-action-primary);
--ds-button-color-bg-hover: var(--ds-color-action-primary-hover);
--ds-button-color-text: var(--ds-color-text-inverse);
--ds-button-space-padding-x: var(--ds-space-3);
--ds-button-space-padding-y: var(--ds-space-2);
--ds-button-border-radius: var(--ds-border-radius-md);

This pattern lets consumers override individual component tokens without touching global tokens:

.my-feature .ds-button {
  --ds-button-color-bg: var(--ds-color-brand-secondary);
}

Pro Tip: Expose component tokens as part of your public API. Document which ones are safe to override and which are internal. I use a _ prefix convention for private tokens: --ds-button-_internal-focus-ring.


Step 5 — Version Your Token Namespace for Breaking Changes

In my experience, teams underestimate how often token names need to change. When a breaking change is unavoidable, I version the namespace rather than doing in-place renames:

/* v1 tokens — deprecated, kept for backward compat */
--ds-v1-color-brand: #3B82F6;

/* v2 tokens — current */
--ds-v2-color-action-primary: #3B82F6;

This lets you migrate incrementally without a big-bang rename that breaks every consumer.


Real-World Tips I Use in Production

Automate token generation with Style Dictionary. I define tokens in JSON and compile to CSS, JavaScript, iOS, and Android from the same source of truth:

{
  "color": {
    "text": {
      "primary": { "value": "#111827", "type": "color" }
    }
  }
}

Running style-dictionary build (v3.9.2 in my current setup) outputs:

$ npx style-dictionary build --config ./sd.config.json
✔ css/variables.css
✔ js/tokens.js
✔ ios/tokens.swift

Use a linter to enforce naming rules. The stylelint-plugin-design-tokens plugin lets you ban raw values and enforce token usage:

# .stylelintrc.yml
rules:
  design-tokens/use-design-tokens:
    - error
    - tokenFiles: ['./tokens/variables.css']

Document every token in a living storybook. I maintain a Storybook “Tokens” section that renders every token visually. This alone reduces designer-developer misalignments by at least 60% on the teams I’ve worked with.


Common Errors and How I Fixed Them

Error: Token names diverge between Figma and CSS. The most maddening bug I’ve dealt with is when a designer names a token Button/Primary/Background in Figma and the engineer creates --button-bg-primary. They’re the same thing, but nothing connects them. My fix: adopt the W3C Design Tokens Community Group format [SOURCE: https://www.w3.org/community/design-tokens/] as the single source of truth and generate both Figma and CSS from it.

Error: Specificity wars with component token overrides. I once had a cascade where --ds-button-color-bg was defined at the :root level and again inside a .dark-theme class, but the component ignored the dark-theme version. The issue was inheritance scope — custom properties inherit but don’t cascade the same way normal properties do. Wrapping the override in the correct DOM ancestor (not just a class on body) fixed it.

Error: Token explosion. After six months, our token file had 1,200+ variables. Half were unused. I now run a quarterly audit using a Node script that parses all CSS/TS files and compares referenced variables against the defined token set — anything unreferenced gets deprecated for one release, then deleted.

[INTERNAL LINK: related article on Design Token auditing strategies]


FAQ

Q: What is the best naming convention for CSS custom properties in a design system? A: The best convention I’ve found is a structured pattern: --[namespace]-[category]-[property]-[variant]-[state]. This creates predictable, self-documenting names. For example, --ds-color-text-primary-disabled tells you immediately: it’s a design system token (ds), in the color category, for text, in the primary variant, when disabled. Pair this with a three-tier hierarchy (global → semantic → component) and you have a system that scales gracefully.

Q: Should CSS variables in a design system use semantic names or descriptive color names? A: Always prefer semantic names. A token named --ds-color-action-primary survives a rebrand or dark mode addition; --color-blue-500 does not. Descriptive names like blue-500 belong only at the global tier and should never be consumed directly by components. Semantic names describe why a token exists, not what it looks like — that distinction is what makes them scalable.

Q: How do you handle dark mode with CSS custom property naming? A: I define all semantic tokens at the :root with light-mode values, then override them inside a [data-theme="dark"] selector. Because components only reference semantic tokens, no component code changes at all when switching themes. The entire dark mode implementation is a single CSS file that re-maps ~30 semantic tokens.

Q: How many tiers of design tokens should a large design system have? A: Three tiers is the sweet spot in my experience: global (raw values), semantic (role-based aliases), and component (scoped overrides). Some large systems add a fourth “brand” tier for multi-brand support, which sits between global and semantic. More than four tiers creates complexity without meaningful benefit for most teams.

Q: How do you prevent CSS variable name conflicts with third-party libraries? A: Always use a unique namespace prefix — I use --ds- for design system tokens and --app- for application-level overrides. Third-party libraries rarely use prefixed namespaces this specific. In addition, running the stylelint-plugin-design-tokens linter in CI catches any raw value usage or undeclared variable references before they reach production.


Conclusion

Naming CSS variables well is one of those investments that pays off slowly and then all at once. The first month, it feels like overhead. The first time you roll out a theme change across 60 components in 20 minutes, you’ll never go back to ad-hoc naming again. Start with the three-tier hierarchy, apply the --[namespace]-[category]-[property]-[variant]-[state] pattern consistently, and automate token generation as early as possible.


About the Author

I’m a senior frontend engineer with 9 years of experience building component libraries and design systems for SaaS products. My day-to-day stack includes React, TypeScript, CSS Modules, and Storybook, with a heavy focus on scalable architecture and developer experience. I’ve led design system migrations at companies ranging from 10-person startups to 400-engineer organizations, and token naming strategy is one of my favorite rabbit holes.