The developer’s guide to design tokens and CSS variables

Design tokens are a platform-agnostic representation of your design decisions, while CSS variables provide a way to implement these decisions in the browser.

The image represents some screenshots from the token set and css code in the background. On the right side we have the title of the article.

Design systems provide a shared language and reusable components that help designers and developers work together toward a common goal: creating cohesive, scalable, and maintainable user interfaces across multiple platforms.

Systematic design approaches have become increasingly essential as platforms have become more varied, products have grown more complex, and teams have become more distributed. Two useful concepts that can help us create effective design systems are design tokens and CSS variables.

Let’s explore the concepts of design tokens and CSS variables, compelling reasons for using them, clarify their key differences, and set some best practices for using both in your design system.

The difference between design tokens and CSS variables

Some developers may confuse design tokens and CSS variables because they share a core concept: representing reusable values. However, they operate at different levels of abstraction and serve different roles within the design and development workflow.

Design tokens: The abstract language of design

Design tokens are the smallest indivisible elements of a design system, representing individual design decisions as data. They function as named entities that store specific visual design attributes such as colors, typography (including font families, sizes, weights), spacing units, border radii, stroke widths, and more. Think of them as a glossary for a design system’s visual style.

Design tokens for color, typography, spacing, border radius, dimensions, font family, font size, font weight, sizing, and stroke width. All used in an audiobook player design.
All the utility tokens used for this app layout design in Penpot.

Their main job is to create a single source of truth for these design decisions, acting as a centralized repository that bridges the gap between design intent and code implementation. Instead of designers specifying concrete values (for example, hex code #6c0c00) and developers hard-coding them, both disciplines reference a meaningful token name (for example, color-text-error). This token then resolves to the appropriate value for the target platform.

Design tokens also support aliasing—the ability for one token to reference another. This can create hierarchies of design decisions. Here’s how it works:

At the top level, we might define design decisions like:

background.surface {colors.white}
card.background {theme.bg.surface}

These simple declarations show two token references:

  • The background.surface token references a primitive color token called colors.white
  • The card.background token references a theme token called theme.bg.surface

In design tokens’ JSON format, these references are described through a structured token definition:

{
  "colors": {
    "white": {
      "value": "#FFFFFF",
      "type": "color"
    }
  },
  "theme": {
    "bg": {
      "surface": {
        "value": "{colors.white}",
        "type": "color"
      }
    }
  },
  "card": {
    "background": {
      "value": "{theme.bg.surface}",
      "type": "color"
    }
  }
}

In this token structure, we see three layers of abstraction:

  1. A primitive token (colors.white) with a raw value (#FFFFFF)
  2. A theme token (theme.bg.surface) that references the primitive with {colors.white}
  3. A component token (card.background) that references the theme token with {theme.bg.surface}

This aliasing structure can enable theming capabilities while maintaining the tokens’ semantic meaning. Changing just the theme token’s value would cascade through the entire system, affecting all elements that reference it directly or indirectly. For instance, to create a dark theme, you’d only need to change theme.bg.surface to a reference dark value like "colors.black,” and all surfaces and cards would automatically update.

Design tokens are platform-agnostic. They are typically defined and stored in neutral, machine-readable formats like JSON and have a specific W3C standard for adoption. This abstraction allows a single set of design decisions to be transformed and applied consistently across diverse platforms, including web (CSS, Sass, JS), native mobile (iOS, Android), and potentially others.

CSS variables: Dynamic styling for the web

CSS custom properties, commonly referred to as CSS variables, bring dynamic styling capabilities to the browser. Unlike design tokens, which exist as abstract data, CSS variables live inside the stylesheet itself. They’re declared using a custom property syntax prefixed with two dashes (--) and accessed using the var() function:

:root {
  --font-size-base: 1.5rem;
  --primary-color: #0052CC;
  --spacing-unit: 0.5rem;
}

button {
  border: none;
  border-radius: 1rem;
  background-color: var(--primary-color);
  color: white;
  font-size: var(--font-size-base);
  margin-top: var(--spacing-unit);
  padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 4);
}

/* For smaller screens, we can adjust our base units */
@media (max-width: 600px) {
  :root {
    --spacing-unit: 0.25rem;
    --font-size-base: 1rem;
  }
  /* All elements using these variables will automatically adjust */
}

Try changing the viewport/browser width to see the button font size and padding adapt.

This example demonstrates three characteristics that make CSS variables unique:

Live and reactive: Unlike preprocessor variables (Sass, Less), which are replaced during compilation, CSS variables remain changeable in the browser. In the example above, changing --spacing-unit in the media query automatically updates all calculations that reference it. This reactivity enables real-time theme switching and responsive adjustments without requiring complex overrides or page reloads.

Cascading and inheritance: CSS variables follow the cascade, which means their values can be overridden by more specific selectors. They also inherit down the DOM tree from parent to child. You can define your variables on :root to apply them globally but then override them for specific components:

.dark-theme {
  --primary-color: #2684FF;
}
button.danger {
  --primary-color: #DE350B;
}

Scoped context: In this way, variables can be scoped to specific components, creating encapsulation that prevents styles from conflicting. While global variables set on :root provide system-wide consistency, component-specific variables enable local customization without affecting other elements.

CSS variables are the perfect way to implement design tokens on the web. While tokens define what your design values mean across platforms, CSS variables make these values work dynamically in the browser, keeping your design decisions connected to your actual code.

The relationship: Tokens as the source, variables as the web implementation

The process of connecting design tokens to CSS variables typically starts with design decisions being shared as structured token data:

{
  "color": {
    "brand": {
      "primary": {
        "value": "#0052CC",
        "description": "Primary brand color used for buttons, links, and key UI elements"
      }
    }
  }
}

During the build process, a transformation tool (like Style Dictionary, Theo, or a custom script) converts these abstract tokens into platform-specific formats. For the web, this means generating CSS variables:

:root {
  --color-brand-primary: #0052CC;
}

These variables are then used throughout the project’s CSS:

.button {
  background-color: var(--color-brand-primary);
}
a {
  color: var(--color-brand-primary);
}

The benefit of this approach becomes clear when you need to make changes. If the brand color needs to be updated from blue (#0052CC) to purple (#5243AA), you only need to modify the token value in one place. The build process regenerates all platform-specific implementations, including CSS variables, and the changes are synced throughout the entire project.

Meanwhile, you can also simultaneously transform the same token into formats for other platforms:

// iOS Swift code
let colorBrandPrimary = UIColor(red: 0, green: 82/255, blue: 204/255, alpha: 1)

This keeps things organized:

  • Design tokens define what the design values are and their semantic meaning.
  • CSS variables determine how these values are implemented in the browser.
  • The transformation process connects them, maintaining consistency across platforms.

With this architecture, design systems achieve both the cross-platform benefits of tokens and the dynamic flexibility of CSS variables, helping create properly scalable, maintainable user interfaces.

Why adopt tokens and variables?

Design tokens and CSS variables offer major benefits that can help address common development challenges:

Achieving consistency across platforms

Design tokens establish a single source of truth for design decisions, enabling consistent implementation across web, native apps, and any other platforms. Combining their platform-agnostic nature with automated transformation processes, helps ensure that styling remains uniform. This in turn reduces design drift and visual bugs.

As your system grows, tokens can help you keep everything consistent, protecting your brand across all platforms. For example, a color-brand-primary token can transform into CSS variables for web, UIColor definitions for iOS, and XML color resources for Android, maintaining exact color matching across all platforms from a single definition.

Improving efficiency and development workflows

The token-based approach significantly speeds up development by enabling designers and developers to reference predefined values rather than repeatedly looking up specific codes.

Tokens have the most impact when making updates—changing a token value automatically propagates across entire products or platforms in minutes rather than days of manual updates. This architecture can fundamentally transform how design evolution is managed, replacing cumbersome handoff processes with more streamlined workflows.

For large-scale applications, tokens help reduce redundancy in code, improving maintainability and allowing developers to focus on implementing core functionality rather than pixel-perfect styling. This efficiency is particularly valuable during design system evolution, where global updates can be managed through token changes rather than manually hunting for occurrences across the codebase.

Creating a shared language

Tokens provide a common, unambiguous vocabulary that bridges design and development. When you use semantic naming conventions, token names ideally clearly convey the purpose of a style (for example, spacing-inset-medium, color-background-pressed.) This should minimize misinterpretation during both design and development.

Codifying design tokens in this way helps transform subjective style choices into objective, shareable data points. Which should then contribute towards teams communicating based on a precise, agreed-upon framework. Modern design tools like Penpot support design tokens directly so designers can create and work with the same token definitions that developers use. During design reviews, teams can reference token names rather than try to make sense of hex codes or pixel values.

Enabling theming capabilities

The combination of token aliasing and CSS variables can create a strong foundation for theming. By structuring tokens so that semantic tokens reference primitive tokens, theme switching only requires redefining primitive values within the theme selector. Since CSS variables are live, the browser automatically recalculates styles based on currently scoped values, making theme changes instantaneous without requiring component recompilation.

A well-structured token system typically uses layers of abstraction including global primitives ( --color-blue-600: #0052CC), semantic mappings (--color-interactive: var(--color-blue-600)), and component-specific tokens (--button-background: var(--color-interactive)) — enabling powerful theming capabilities with minimal code. This approach not only facilitates dark/light modes but also enables accessibility features like high-contrast themes, color blindness accommodations, and user preference settings that can respond dynamically to system preferences through CSS media queries like prefers-color-scheme.

Try switching your device between light and dark mode to see the colors adapt.

Reducing technical debt

A token-based design system can significantly reduce technical debt by creating a more maintainable architecture for style management. Without tokens, styling information tends to become randomly scattered throughout codebases, leading to inconsistent implementation, duplicate values, and unused code that’s difficult to identify and remove.

By centralizing design decisions in tokens, teams can create a clear structure that makes dependencies more visible and refactoring safer. You can also enhance the transformation process that converts tokens to platform-specific variables with validation and linting, catching errors before they reach production. Using a structured approach like this can prevent the accumulation of styling anomalies that would require loads of work to reconcile later.

Implementing tokens as CSS variables

Using design tokens for web development requires a clear process for translating the abstract token definitions into concrete CSS variables that browsers can understand and utilize. This involves choosing a transformation workflow, establishing naming conventions, structuring tokens, managing CSS scope, and organizing your resulting CSS files.

Manual setup vs. build tools

Implementing design tokens in CSS can be approached in two ways: manual setup or automated build tools.

For small projects, a manual approach involves hand-coding token values directly into CSS variables within a stylesheet. You simply define your tokens in the :root selector as custom properties and reference them throughout your CSS. This approach is straightforward for the first few tokens but quickly becomes unwieldy as your design system grows.

For larger projects, build tools like Style Dictionary offer significant advantages. Style Dictionary takes centralized token definitions (usually in JSON format) and automatically generates platform-specific outputs, including CSS variables for web. The tool ensures consistency by transforming tokens from a single source of truth into multiple platform formats.

/* Example output from Style Dictionary */
:root {
  /* Primitive tokens */
  --color-blue-500: #2980b9;
  /* Semantic tokens */
  --color-primary: var(--color-blue-500);
}

Check out Stu Robson’s guide for using Style Dictionary with Penpot’s design tokens.

While manual setups require no tooling and offer full control, they quickly become error-prone for larger token collections. Build tools may introduce initial setup costs but they dramatically improve scalability and consistency. They can also support advanced features like token aliases, grouping, and theming.

Hierarchical naming patterns

Try to adopt a consistent, multi-level naming convention that clearly conveys a token’s purpose and organization. Most design systems use a structure that progresses from broad to specific:

[category]-[property]-[element]-[modifier]-[state]

For example, --color-background-button-primary-active immediately communicates this token is:

  • A color token
  • For backgrounds
  • For buttons
  • For the primary variant
  • In the active state

Structuring names like this prevents naming collisions, improves organization, and makes the token’s purpose clear. When translating token names to CSS variables, use kebab-case with hyphens as a consistent separator.

Avoid overly generic names like --primary or --large, and instead use descriptive, context-based naming like --color-primary or --spacing-large that clarifies both category and purpose.

Separating primitive and semantic tokens

Distinguish between primitive tokens (raw values) and semantic tokens (contextual usage):

:root {
  /* Primitive tokens: raw design values */
  --color-blue-400: #3498db;
  --color-blue-500: #2980b9;
  --spacing-4: 1rem;
  
  /* Semantic tokens: contextual usage */
  --color-primary: var(--color-blue-500);
  --color-primary-hover: var(--color-blue-400);
  --layout-page-padding: var(--spacing-4);
}

Primitive tokens define your fundamental design palette without context (colors, spacing units, and so on), while semantic tokens map these primitives to specific UI roles. This separation creates a valuable layer of abstraction that helps you:

  • Update primitive values while preserving semantic references
  • Create themes by remapping semantics to different primitives
  • Make design language changes without modifying component code

Try to always ensure semantic tokens reference primitives rather than hardcoding values. This maintains the one-directional relationship where primitives define options and semantics define usage.

Defining tokens at the right scope

You can make use of CSS’s natural scoping to apply tokens at the appropriate level:

/* Global tokens */
:root {
  --color-primary: #3498db;
  --font-size-base: 1rem;
}
/* Themed tokens */
[data-theme="dark"] {
  --color-background: #1a1a1a;
  --color-text: #f5f5f5;
}
/* Component-specific tokens */
.button {
  --button-border-radius: 0.3rem;
}

Global tokens that apply throughout the application (brand colors, type scales) should be defined on the :root selector. Component-specific tokens can be scoped to their respective selectors to prevent namespace pollution.

For theming, define base tokens globally, then override specific tokens within theme selectors. The cascading nature of CSS variables allows the same token name to have different values in different contexts.

One approach I’d generally recommend is to use variables for reusable values that appear in multiple places or may change, but use literal values for one-off styles to avoid unnecessary abstraction. For example:

/* Good practice */
:root {
  --border-radius-button: 0.3rem;  /* Used across multiple button variants */
}
.button {
  border-radius: var(--border-radius-button);
  padding: 0.4rem 0.8rem;  /* One-off padding specific to this component */
}
.button-large {
  border-radius: var(--border-radius-button);
  padding: 0.6rem 1.2rem;  /* Different one-off padding */
}

In this example, border-radius is tokenized because it’s used consistently across button variants, while the padding values remain as literals since they’re specific to each variant.

Organizing tokens semantically

It can help to structure your token definitions in your CSS files to mirror your design system’s organization:

:root {
  /* Color Tokens */
  --color-primary: #3498db;
  --color-secondary: #2ecc71;
  
  /* Typography Tokens */
  --font-family-base: 'Inter', sans-serif;
  --font-size-base: 1rem;
  
  /* Spacing Tokens */
  --spacing-1: 0.25rem;
  --spacing-2: 0.5rem;
}

Here are some ways to organize your tokens:

  1. Design categories: Group color tokens together, typography tokens together and so on, either in separate files (like tokens-colors.css) or clearly marked sections.
  2. Token types: Separate primitive and semantic token definitions to clarify the relationship between values and their usage.
  3. Theming/modes: Organize token overrides for each theme (light/dark) in dedicated files or sections.
  4. Component scope: Keep component-specific tokens alongside their component CSS when they won’t be reused elsewhere.

Use comments liberally to label token sections and document their purpose. This semantic organization ensures the CSS reflects the design system’s conceptual structure, making the codebase more maintainable as it evolves.

Creating and managing design tokens in Penpot

Design tokens are foundational building blocks for design systems. As the first design tool to natively support the W3C DTCG standard, Penpot offers an environment for creating and managing design tokens, connecting design and development.

Token types included in this design are border radius, color, dimensions, font family, font size, font weight, sizing, spacing, and stroke width.
The tokens panel in Penpot groups tokens by type.

Token creation and organization

Penpot implements design tokens following the W3C DTCG Format Module, ensuring standardized cross-platform compatibility. You can create tokens from the Tokens panel with three key attributes:

  • Name: Must be unique within a token type (for example, dimension.small)
  • Value: Can be numerical values or color spaces (Hex, RGB, RGBA, ARGB, HSL, HSLA)
  • Description: Optional documentation for the token’s purpose
Editing the berry-dot-500 tokens shows its value is #F65391 and it has no description as the field is optional.
Editing a color token in Penpot.

Penpot uses token sets so you can separate tokens into collections that work for your system and team:

Sets/
├── Global/
│   ├── Color/
│   └── Dimension/
├── Brand/
└── Mode/
    ├── Light/
    └── Dark/

Sets follow a cascading priority model — tokens with identical names in different sets will be resolved based on the order of the sets, with later sets overriding earlier ones, similar to the CSS cascade.

Dark mode tokens including color tokens for different levels for backgrounds, borders, and texts.
The tokens in the light and dark mode sets have the same names, but different values.

Using token references and equations

Penpot supports token referencing (aliases) by using curly braces to reference existing tokens:

spacing.small: {dimension.small}

This creates handy token dependencies where changing a primitive token automatically updates all derivatives. And these updates are also reflected in any objects using tokens on the canvas. References are case-sensitive and must be enclosed in {}.

A level-1-dot-background token with a value of {neutral-dot-900}.
The level-1.background token references the neutral.900 token.

For numerical tokens, Penpot also supports mathematical equations:

spacing.medium: {spacing.small} * 2

spacing.large: {spacing.small} * {spacing.scale}

Equations support basic operators (+, -, *, /), which can help create proportional relationships between tokens.

An md token with the value {sm} * {multiplier}.
The md dimension token is created by multiplying the sm token by the multiplier token.

Theming with token sets

Penpot’s theming system allows you to combine token sets in themes, which can also be grouped:

Mode/
├── Light
└── Dark
Brand/
├── RedPlanet
└── YellowCab

This approach allows efficient management of theme variants without you having to create every possible combination. When a token exists in multiple active themes, the theme appearing last in the list takes precedence.

To create effective theme structures:

  1. Define global or primitive tokens in base sets
  2. Create semantic tokens that reference primitives
  3. Organize theme-specific overrides in dedicated sets
  4. Group related themes (for example Light/Dark under “Color mode”)
0:00
/0:13

Switching between the light and dark themes toggles the corresponding token set.

Design tokens and CSS variables together create a powerful system for maintaining design consistency across platforms while enabling dynamic theming and responsive design on the web. By bridging the gap between abstract design decisions and concrete implementation, they can help establish a shared language that both designers and developers can understand and reference.

Ready to start? Try Penpot today to start creating and managing W3C-standard design tokens that integrate with your development workflow.

We have loads more posts about design tokens, CSS, and web development topics. Here’s a few examples of helpful articles to help .

Why use Design Tokens? 7 practical reasons designers and developers swear by
From reducing human error to creating more harmony between teams, let’s explore the benefits of design tokens and why you might want to consider using them in your next project.
The top 5 questions people have about CSS answered by Penpot
Having a basic knowledge of CSS can really benefit your skills in web and UI design. Let’s start with these answers for the top CSS questions asked by beginners today.
Intro to Design Tokens
Design tokens are tiny reusable building blocks. You can use them to keep colors, spacing and other styles consistent across your design, and combine them to create design systems.
Design Tokens with Penpot
Design tokens are the smallest repeatable elements of your design that can be reused consistently across your Penpot projects and teams.
What is CSS? A beginner’s guide
While learning CSS takes time, understanding how it works should be easy. In this guide, we’ll explain the basics of CSS to help you start your journey.