Available For Front-End & Design Systems Contracts – Get In Touch

Always Twisted

Design Token Naming Conventions: A Practical Guide

On This Page:

If you have ever stared at a long token list and wondered whether a name should be button-primary-bg, primary-button-background, or color-button-primary, you are not alone.

Naming design tokens can look and feel simple right up until you have to do it for real. Choose a weak pattern and things get inconsistent fast. Choose a clear pattern and you get a shared language that helps both design and engineering move quicker.

There is no single correct convention. A two-person team shipping one product has very different needs from a large organisation running multiple brands across multiple platforms.

What you can do is pick a convention that is clear, consistent, and scalable. This article walks through the core token tiers, common naming models, and practical rules that help avoid naming drift.

Why naming gets hard faster than expected

Most teams don't struggle creating tokens. They struggle to keep token meaning stable as more people and components work on the Design System.

You can often see multiple names emerge for the same idea:

Code languagejson
["spacing.small", "space-2", "gap-md", "paddingStandard"]

Each name might make sense in isolation. Together, they signal a semantic drift. The naming model is no longer acting as a shared language.

This is why naming quality can affect delivery speed. If token intent is unclear, every usage becomes a local judgement call, and those judgement calls will accumulate into inconsistency.

The examples in this article follow the Design Tokens Community Group specification using $value, $type, and dot-notation aliases like {color.brand.primary}.

What are design tokens, a quick recap

Design tokens are a tooling and platform agnostic way to store design decisions as named values: colour, spacing, typography, radius, shadows, motion, and (much) more.

The key difference is where and how many times you define those values.

Without tokens, you might use #BADA55 in dozens of places across components, stylesheets, and design files. When that green needs to change, you might have to hunt down every instance to make sure you're updating everything that needs to be updated.

With tokens, you create a design token file, a JSON object following the Design Tokens Community Group specification:

Code languagejson
{
  "green": {
    "400": {
      "$value": "#BADA55",
      "$type": "color"
    }
  }
}

You define #BADA55 once as green.400, then you can reference that name everywhere. The value is still defined somewhere, but it is defined in one place with a meaningful name and consumed by both design tools and code by reference via tooling like Tokens Studio or Style Dictionary.

This centralisation helps to give you three major benefits:

Tokens are not just a technical detail. They are a shared language between design and development (and the whole team) that ends up in the product(s).

The three token tiers

Most modern token workflows follow a three-tier model. You will see different names for each tier, but the same underlying structure keeps showing up.

Tier 1 - Primitive tokens

These are your raw values.

They describe what something is, not why it exists.

They are also referred to as reference, base, options, or global tokens.

Code languagejson
{
  "blue": {
    "500": {
      "$value": "#0066CC",
      "$type": "color"
    }
  },
  "spacing": {
    "8": {
      "$value": "8px",
      "$type": "dimension"
    }
  }
}

Primitives should not (imho) and are usually not consumed directly by components. They are source values for more specific design decisions and token tiers.

A common failure is skipping straight to component naming because it feels productive in the short term. In practice, that locks in naming decisions before your core scales are stable.

When possible, stabilise primitives first, attach intent later.

Code languagejson
{
  "core": {
    "dimension": {
      "100": { "$value": "8px", "$type": "dimension" },
      "200": { "$value": "16px", "$type": "dimension" },
      "300": { "$value": "24px", "$type": "dimension" }
    }
  }
}

Tier 2 - Semantic tokens

Also known as alias, decision, theme, or system tokens.

Semantic tokens can express intent. They describe why a value is used or what it is for.

Code languagejson
{
  "color": {
    "brand": {
      "primary": {
        "$value": "{blue.500}",
        "$type": "color"
      }
    }
  }
}

This is where tokens become resilient.

If brand blue changes, the semantic name can stay the same while the underlying primitive reference changes.

You can update once and it will change everywhere.

Code languagejson
{
  "layout": {
    "spacing": {
      "formStack": { "$value": "{core.dimension.300}", "$type": "dimension" },
      "contentToButton": {
        "$value": "{core.dimension.200}",
        "$type": "dimension"
      }
    }
  }
}

Tier 3 - Component tokens

Also known as "component specific" or contextual tokens.

This layer maps semantic intent to specific elements, components, and parts thereof.

Component tokens should always reference semantic tokens, never primitives directly. That indirection is what can keep the system flexible.

It is optional, but very useful in larger systems.

Code languagejson
{
  "button": {
    "primary": {
      "background": {
        "$value": "{color.brand.primary}",
        "$type": "color"
      }
    }
  }
}

Component tokens give you precision. You can tune one component without creating side effects and explosions elsewhere.

Common naming conventions

There is no universal winner ... "It depends". Different teams will always optimise for different makeup, needs, and constraints.

These diagrams show the core layers for each convention. Real-world usage may add optional layers like state or namespace for more specificity.

1. Category-Property-Modifier (CPM)

category property role state

This starts broad and gets more specific: category, property, role (and optionally state for interactions). It is easy to scan and keeps naming fairly lightweight.

Code languagejson
{
  "color": {
    "button": {
      "primary": {
        "hover": {
          "$value": "{color.brand.primary-hover}",
          "$type": "color"
        }
      }
    }
  }
}

2. Tier-based naming

primitive semantic component state

Here, the naming pattern changes depending on the tier. Primitives hold values, semantic tokens hold intent, and component tokens hold local UI decisions, which keeps each layer focused. The diagram shows the token tiers; actual nesting depth varies (e.g., primitives are often 2 layers, while component tokens may be 3+).

Code languagejson
{
  "blue": {
    "500": {
      "$value": "#2196F3",
      "$type": "color"
    }
  },
  "color": {
    "brand": {
      "primary": {
        "$value": "{blue.500}",
        "$type": "color"
      }
    }
  },
  "button": {
    "primary": {
      "background": {
        "$value": "{color.brand.primary}",
        "$type": "color"
      }
    }
  }
}

3. Object-Property-Modifier (OPM)

object property modifier state

This one starts with the UI object, then narrows to property and variant. It tends to feel natural in component-led workflows because it mirrors how teams talk about UI day to day.

Code languagejson
{
  "button": {
    "background": {
      "primary": {
        "hover": {
          "$value": "{color.brand.primary-hover}",
          "$type": "color"
        }
      }
    }
  }
}

4. Context-first naming

tier category property modifier

Context (e.g., 'brand' or 'theme') comes first, followed by category, property, and modifier. This ensures you know the layer before the specific decision. That works well when the same semantic token needs to exist across several themes or brands.

Code languagejson
{
  "brand": {
    "color": {
      "primary": {
        "default": {
          "$value": "{primitive.color.blue.500}",
          "$type": "color"
        }
      }
    }
  }
}

5. A Comprehensive Structure

namespace category concept property variant state

Each segment has a specific job, from namespace through to state. This comprehensive structure allows for up to 6 layers, but start with the core 5 and add as needed for complexity. Names are longer, but they are very explicit, which helps when multiple teams and brands share the same system. Nathan Curtis's article Naming Tokens in Design Systems goes much deeper on this taxonomy, it's well worth a reading (and bookmarking).

Code languagejson
{
  "acme": {
    "color": {
      "button": {
        "background": {
          "primary": {
            "hover": {
              "$value": "{acme.color.brand.primary}",
              "$type": "color"
            }
          }
        }
      }
    }
  }
}

Handling variants, states, and modes

Whatever base naming model you choose, you still need clear, predictable state and variant placement patterns.

One practical way to stay coherent is to define where each concept lives in the name:

  1. Variant: what version of the same thing it is (for example primary, secondary, danger)
  2. State: how that version is currently behaving (for example hover, active, disabled)
  3. Mode: which environment it belongs to (for example light, dark, highContrast)

If those three ideas float around in different positions, naming gets noisy fast.

The same decision in different conventions

These examples all express the same meaning: primary button background on hover in dark mode.

Code languagejson
[
  "button.primary.background.hover.dark",
  "button-background-primary-hover-dark",
  "dark.button.primary.background.hover",
  "acme.color.button.background.primary.hover.dark",
  "theme.dark.component.button.primary.background.hover"
]

Pick one ordering model and enforce it consistently. Mixing these patterns inside one system is where discoverability starts to break down.

Variants

Variants describe alternatives at the same hierarchy level, not interaction changes.

Code languagejson
[
  "button.primary.background",
  "button.secondary.background",
  "button.danger.background"
]

Equivalent variant naming in other conventions:

Code languagejson
[
  "button-background-primary",
  "button-background-secondary",
  "button-background-danger",
  "component.button.background.primary",
  "component.button.background.secondary",
  "component.button.background.danger"
]

States

States represent interaction or status changes for the same UI element. Keep state terms predictable (hover, active, focus, disabled) so engineers can find related tokens quickly.

Code languagejson
[
  "button.primary.background.hover",
  "button.primary.background.active",
  "button.primary.background.disabled",
  "button.primary.background.focus"
]

The same state grouping works across naming styles:

Code languagejson
[
  "button-background-primary-hover",
  "button-background-primary-active",
  "button-background-primary-focus",
  "button-background-primary-disabled"
]
Code languagejson
[
  "component.button.background.primary.hover",
  "component.button.background.primary.active",
  "component.button.background.primary.focus",
  "component.button.background.primary.disabled"
]

Scale sizes

Scale tokens create consistent step-based sizing. Whether you use xs-xl labels or numeric steps, use one scale pattern and apply it everywhere to avoid drift.

Code languagejson
{
  "spacing.sm",
  "spacing.md",
  "spacing.lg",
}

For type scales, the same rule applies: keep naming sequential so size relationships are obvious at a glance.

Code languagejson
["font.size.sm", "font.size.md", "font.size.lg"]

Modes and themes

Modes are contextual variants of the same decision (for example light and dark). The token purpose stays the same, only the value changes per mode.

Code languagejson
["color.background.default.light", "color.background.default.dark"]

Other mode-placement patterns you will see in real systems:

Code languagejson
[
  "light.color.background.default",
  "dark.color.background.default",
  "theme.light.color.background.default",
  "theme.dark.color.background.default"
]

Whichever pattern you choose, keep mode placement fixed. A stable suffix or a stable prefix both work; switching between them does not.

Numbered scales

Numbered scales are useful when you need many fine-grained steps, especially for colour ramps and heading systems. The key is to keep the numbering direction and increments consistent.

Code languagejson
[
  "font.size.heading.1",
  "font.size.heading.2",
  "font.size.heading.3",
  "color.gray.50",
  "color.gray.100",
  "color.gray.200"
]

Numbered scales also appear in alternative patterns:

Code languagejson
[
  "size-font-heading-100",
  "size-font-heading-200",
  "size-font-heading-300",
  "scale.type.heading.100",
  "scale.type.heading.200",
  "scale.type.heading.300"
]

Rules that keep token naming healthy

Most strong naming systems follow a predictable anatomy: an optional prefix (namespace or context), a core meaning (category and property), optional modifiers (intent, variant, scale), and an optional suffix (state or mode).

You do not need every part in every token, but when a part is present it should always appear in the same position.

1. Consistency beats perfection

The best naming system is the one your whole team actually follows.

Drifting tends to start not where the convention is broken outright, but where it is ambiguous enough to invite interpretation. A token set that mixes naming styles makes it impossible to predict what a new token should be called.

Avoid: mixing conventions across the same token tier

Code languagejson
[
  "color.button.primary.default",
  "button.background.primary.hover",
  "theme.dark.component.button.primary.background.default"
]

Prefer: one convention applied throughout

Code languagejson
[
  "component.button.primary.background.default",
  "component.button.primary.background.hover",
  "component.button.secondary.background.default"
]

2. Optimise for clarity over brevity

Autocompletion means typing long names is rarely a problem, but reading a token list at a glance still relies on human pattern recognition. Abbreviating too aggressively trades a small typing convenience for an ongoing cognitive overhead every time someone reads or audits the system.

Avoid abbreviations that require decoding

Code languagejson
["btn-bg-pri-hvr", "c-txt-err", "sp-md"]

Prefer names that read on first glance

Code languagejson
["button-background-primary-hover", "color-text-error", "spacing-md"]

3. Keep semantics semantic

A name like color-text-red describes a visual value, which means the name breaks as soon as the colour changes. A name like color-text-error describes intent, which stays stable even when the underlying value does not. At semantic and component levels, name for why, not what.

Avoid: naming for the visual value

Code languagejson
["color.text.red", "color.background.grey", "color.border.green"]

Prefer: naming for the intent

Code languagejson
["color.text.error", "color.background.subtle", "color.border.success"]

4. Order from broad to specific

Names are easier to scan when they move from the widest concept toward specificity. This also means that alphabetically sorted token lists naturally group related tokens together, which makes auditing and searching much faster.

Avoid: specificity before category

Code languagejson
["hover.primary.button.color", "disabled.background.input", "lg.font.heading"]

Prefer: broad category first, narrowing toward specificity

Code languagejson
[
  "color.button.primary.hover",
  "color.input.background.disabled",
  "font.heading.lg"
]

5. Make names self-explanatory

A developer who has never seen your design tokens before should be able to make a reasonable guesstimate about what it does. If a name requires specific knowledge or a colleague to interpret, it is a candidate for renaming.

Avoid: names that only make sense in context

Code languagejson
["token-1", "c-bd-x", "primary-a"]

Prefer: names that explain themselves

Code languagejson
[
  "button.border.radius",
  "color.border.focus",
  "color.button.background.primary"
]

6. Design for growth

Your naming model should be able to absorb more components, more themes, and potentially more brands without a heavy structural rewrite. That does not mean you need to "over engineer" from the start, but it does mean avoiding patterns that only work at small scale, like implicit positional meaning or naming steps after their current count.

Avoid: patterns that hit a ceiling

Code languagejson
["spacing-small", "spacing-smaller", "spacing-new", "spacing-new-2"]

Prefer: a scale that can always grow in either direction

Code languagejson
["spacing.100", "spacing.200", "spacing.300", "spacing.420"]

Choosing your convention

A practical way to decide is to consider your team size, system scope, tooling, and existing patterns together.

Larger teams will need a stronger structure, single product systems can probably stay simpler, and tools like Figma, Tokens Studio, and your build pipeline may naturally favour one style.

It is also usually safer to evolve existing patterns than replace everything at once, and the best outcome comes when designers and developers both buy into the same model.

Start simple, document it clearly, add examples, and revisit as the system grows.

Token names do not need to be academically perfect. They need to be clear enough that your team can apply them without hesitation.

Keep naming alive with lightweight governance

Naming is not a one time decision.

It is an ongoing Design System practice.

If you want conventions to hold up over time, define lightweight governance:

  1. One canonical naming guide with approved examples
  2. A short review checklist for new tokens
  3. Periodic cleanup of duplicate or ambiguous names
  4. Shared ownership across design and engineering

The goal is not "process for process' sake".

A token system in a Design System that people trust is one they will actually use, and consistent naming is how that trust gets built.

Struggling with responsive design, breakpoints, or mobile optimisation?

I'll help you build a mobile-first, responsive approach that works smoothly across devices.

get in touch!