Frontend

Design Tokens and Theming: One Source of Truth for Color, Spacing, and Dark Mode

Learn how design tokens, CSS variables, semantic tokens, and theming create a consistent UI and simplify light and dark mode.

Aarav Sharma

Aarav Sharma

October 26, 202611 min read
Share
A primitive token holding a raw blue, a semantic token pointing at it, and a component reading only the semantic token

When an application grows, the same colors, spacing values, borders, and other design decisions often appear in many components. Hardcoding those values may work at first, but changing a shared design value later can require editing many files and can easily leave the UI inconsistent.

Design tokens and theming give you a single source of truth for those shared values. You define important design decisions once, give them meaningful names, and let components reuse them. This also makes light and dark themes easier because the same components can use different token values without needing separate styles for every component.

Quick Answer

Design tokens are reusable values for colors, spacing, typography, borders, and other UI properties that keep an application's design consistent. Instead of hardcoding these values in individual components, you define them once and reuse them throughout the application.

For example, rather than using #2563EB separately for buttons, links, icons, and headers, you can define a --color-primary token and reference it wherever the primary color is needed. When the design changes, you update the token instead of searching through every component.

The same approach works for theming. Components use semantic tokens such as --color-background and --color-text-primary, while light and dark themes provide different values for those tokens.

What Are Design Tokens and Theming?

Design tokens are reusable, named values that represent common design decisions such as colors, spacing, typography, and borders. Instead of defining those values separately in every component, you define them centrally and reuse them across the application.

Theming builds on this approach by allowing the same components to use different token values for themes such as light and dark mode.

For example:

  • Design tokens provide a central place for common UI values.
  • Theming changes those values based on the active theme.
  • Semantic tokens describe the purpose of a value rather than its raw

value.

  • A centralized token system becomes more useful as an application

gains more components, pages, developers, or themes.

The main benefit is simple: when a shared design value changes, you can update it centrally instead of manually changing it across many components.

The Problem

As an application grows, you add more pages and components. You also add more colors, spacing values, fonts, borders, and other styles.

At the beginning, it is common to write those values directly inside components:

.button {
  background: #2563EB;
  padding: 16px;
}

.card {
  background: #FFFFFF;
  padding: 16px;
}

.header {
  background: #2563EB;
}

For a small application, this is manageable. As the application becomes larger, the same values start appearing in many places.

The same hardcoded color and padding values repeated across a button, a card, and a header, leaving the UI inconsistent and slow to change

The Same Values Are Repeated

The same blue might be used in buttons, links, and headers:

.button {
  background: #2563EB;
}

.link {
  color: #2563EB;
}

.header {
  background: #2563EB;
}

If the requirement changes the primary color, you need to find every occurrence of #2563EB. If you miss one, part of the application can continue using the old color.

Different Components Can Look Different

Spacing can also become inconsistent:

.card {
  padding: 16px;
}

.modal {
  padding: 18px;
}

.sidebar {
  padding: 20px;
}

All three components need spacing, but they use different values. Small differences like these can make the UI inconsistent as the application grows.

Changes Take More Time

A requirement such as "change the primary color of the application" can affect buttons, links, headers, navigation, icons, forms, and cards. When those values are hardcoded, you need to inspect many files and make the same change repeatedly.

Dark Mode Becomes More Difficult

Dark mode introduces another set of values. A card might use these styles in light mode:

.card {
  background: #FFFFFF;
  color: #111827;
}

Then it needs different values in dark mode:

.dark .card {
  background: #1F2937;
  color: #F9FAFB;
}

If many components contain hardcoded colors, you may need separate dark-mode rules for many of them.

The core problem is that the same design decisions are written in many different places. Changes take more time, values can become inconsistent, and dark mode becomes harder to maintain.

The Solution

The solution is to keep common design values in one central place and let components use those values instead of writing them directly.

Design decisions becoming primitive tokens, primitive tokens becoming semantic tokens, and components using only the semantic tokens

Instead of this:

.button {
  background: #2563EB;
  padding: 16px;
}

Create shared tokens:

:root {
  --color-primary: #2563EB;
  --spacing-md: 16px;
}

Then use them in components:

.button {
  background: var(--color-primary);
  padding: var(--spacing-md);
}

Now, if you change the primary color:

:root {
  --color-primary: #7C3AED;
}

all components using var(--color-primary) receive the new value.

Theming With CSS Variables

The same idea works for light and dark mode:

:root {
  --color-background: #FFFFFF;
  --color-text-primary: #111827;
}

[data-theme="dark"] {
  --color-background: #111827;
  --color-text-primary: #F9FAFB;
}

The component does not need separate light and dark styles:

.card {
  background: var(--color-background);
  color: var(--color-text-primary);
}

The component stays the same while the theme changes the token values.

Step-by-Step Implementation

This example uses a small React application with a button and a card. The goal is to keep design values in one place and allow the same components to work with light and dark themes.

Steps from creating a Vite project through a token file, primitive and semantic tokens, light and dark themes, and theme switching

Step 1: Create the Project

Start with a small Vite React project:

npm create vite@latest design-token-demo -- --template react
cd design-token-demo
npm install
npm run dev

You do not need a large application to understand the design token architecture. A few components are enough to demonstrate the approach.

Step 2: Create a Token File

Create a shared file for design values:

src/
└── styles/
    └── tokens.css

Keep reusable values such as colors, spacing, typography, and border radius in this file.

The basic flow is:

Design Values → tokens.css → Components

This gives you one central place for shared design decisions.

Step 3: Create Primitive Tokens

Start with basic values:

:root {
  --blue-500: #2563EB;
  --gray-900: #111827;
  --gray-100: #F3F4F6;

  --spacing-md: 16px;
  --radius-md: 8px;
}

These are primitive tokens because they represent basic values such as a specific blue, gray, spacing value, or radius.

Step 4: Create Semantic Tokens

Next, give those values a meaning:

:root {
  --color-primary: var(--blue-500);
  --color-text-primary: var(--gray-900);
  --color-background: var(--gray-100);
}

Components can now use names such as --color-primary, --color-text-primary, and --color-background instead of depending directly on --blue-500 or --gray-900.

This matters because a component cares about the purpose of a value, not its current color.

Step 5: Add the Light Theme

Define the default light-theme values:

:root {
  --color-background: #FFFFFF;
  --color-text-primary: #111827;
  --color-surface: #F9FAFB;
  --color-primary: #2563EB;
}

These values are used when the application is in light mode.

Step 6: Add the Dark Theme

Define different values for the same semantic tokens:

[data-theme="dark"] {
  --color-background: #111827;
  --color-text-primary: #F9FAFB;
  --color-surface: #1F2937;
  --color-primary: #60A5FA;
}

You are not creating different tokens for dark mode. You are changing the values of the same semantic tokens.

The architecture is:

Theme
  ↓
Light / Dark
  ↓
Token Values
  ↓
Components

Step 7: Use Tokens in Components

Use the tokens instead of hardcoded values:

.button {
  background: var(--color-primary);
  padding: var(--spacing-md);
  border-radius: var(--radius-md);
}

.card {
  background: var(--color-surface);
  color: var(--color-text-primary);
  padding: var(--spacing-md);
  border-radius: var(--radius-md);
}

The components do not need to know the actual color or spacing values. They use the design tokens, which keeps design decisions separate from component code.

Step 8: Add Theme Switching

Control the theme with the data-theme attribute:

<html data-theme="light">

When the user switches to dark mode:

<html data-theme="dark">

Because the components already use semantic tokens, changing the theme automatically changes the values those components use.

Step 9: Test the Theme

Test both themes and check that:

  • The button uses the correct primary color.
  • The card changes its background and text color.
  • Spacing remains consistent.
  • Light mode looks correct.
  • Dark mode looks correct.
  • Switching themes does not require changing component CSS.

Also test the single source of truth. For example, if you change:

--spacing-md: 16px;

to:

--spacing-md: 20px;

every component using var(--spacing-md) should update automatically.

Common Problems and Best Practices

Check Why a Token Is Not Applying

If a component is not using the expected value, check the token name, make sure the token file is imported, verify that the CSS variable is defined in the correct scope, and confirm that the component uses var(--token-name) correctly. A token such as --color-primary has to exist in the active scope, or the declaration is simply dropped.

Check Why Dark Mode Is Not Working

If dark mode does not change the UI, check that the data-theme attribute changes and that the selector matches it:

[data-theme="dark"] {
  --color-background: #111827;
}

The HTML must use the same attribute value:

<html data-theme="dark">

Also make sure the same semantic token names are defined for both themes.

Remove Hardcoded Values From Theme-Aware Components

Sometimes most of the application changes to dark mode but a few components do not. A common reason is that those components still contain hardcoded colors:

.card {
  background: #FFFFFF;
}

Use the token instead:

.card {
  background: var(--color-surface);
}

A component with hardcoded theme values cannot fully benefit from the theme system.

Use Clear Token Names

Avoid unclear names such as:

--blue
--blue2
--new-blue
--dark-blue

Prefer names based on purpose:

--color-primary
--color-surface
--color-text-primary
--color-border

Semantic names make the system easier to understand and maintain. Pick a prefix pattern such as --color-*, --spacing-*, --radius-*, and --font-*, then use it consistently so tokens stay easy to find.

When Should You Use Design Tokens?

Design tokens are useful when an application has many shared design values.

Consider using them when:

  • Your application has many components.
  • The same colors and spacing are used in many places.
  • You need light and dark themes.
  • Your design changes regularly.
  • Multiple developers work on the same project.
  • You want a consistent design across the application.
  • You are building a reusable design system.

You may not need a large token system when:

  • You are building a very small one-page project.
  • Most design values are used only once.
  • The project does not need multiple themes.
  • Adding a token system would create more complexity than value.

FAQ

What are design tokens?

Design tokens are reusable values for common design decisions such as colors, spacing, typography, borders, and shadows. Instead of repeating the same values in different components, you define them once and reuse them.

Why should I use design tokens?

Design tokens make your UI more consistent and easier to maintain. If a shared value changes, you can update the token instead of searching through many components.

Can design tokens be used for dark mode?

Yes. Design tokens make dark mode much easier. You can keep the same token names and provide different values for light and dark themes.

:root {
  --color-background: #FFFFFF;
}

[data-theme="dark"] {
  --color-background: #111827;
}

The component can continue using:

background: var(--color-background);

Should components use primitive or semantic tokens?

Components should generally use semantic tokens. For example:

--blue-500: #2563EB;
--color-primary: var(--blue-500);

A button can use --color-primary instead of directly using --blue-500. This makes the component depend on the purpose of the value rather than its actual color.

Do I need design tokens for every CSS value?

No. You should not turn every CSS value into a token. Focus on values that are shared, important, or likely to change, such as colors, spacing, typography, and border radius.

Are CSS variables enough for a design token system?

For many projects, yes. CSS variables provide a simple way to create reusable values and support themes without needing a separate token library. For larger design systems, you may choose additional tools depending on your project requirements.

Trade-offs

Design tokens solve repeated-value and consistency problems, but they are not automatically useful for every project.

A token system adds another layer between a component and the final CSS value. In a very small one-page project where most values are used only once, adding a large token architecture can create more complexity than value.

You can also create too many tokens. If every small CSS value becomes a named token, developers may spend more time understanding the token system than maintaining the UI.

The useful approach is to start with values that are shared, important, or likely to change. Expand the token system as the application grows.

There are also theme-specific responsibilities. Changing a token does not guarantee that the resulting design is accessible. You still need to check text, buttons, borders, backgrounds, hover states, focus states, disabled states, and error states in both themes.

There is a performance trade-off as well. Custom properties are inherited and resolved at computed-value time, so changing one on :root invalidates the computed style of every element that reads it. A theme switch is therefore a full-page style recalculation, which is fine at the rate a person toggles a theme.

The cost appears when a token changes many times per second. A custom property cannot be animated on the compositor the way transform and opacity can, so driving one from scroll or pointer movement forces style and layout work on every frame. Keep tokens for design decisions that change rarely, and animate concrete properties directly. The MDN guide to using CSS custom properties explains how inheritance and resolution work.

Conclusion

Start by moving the shared colors, spacing, typography, and other important design decisions into a central token file. Use semantic token names in components so the components depend on the purpose of a value rather than its current raw value.

Then add light and dark themes by changing the values of those same semantic tokens. Test both themes, remove unnecessary hardcoded values, and check contrast and important UI states before shipping.

You do not need a large token system from the beginning. Start small, keep the naming clear, and expand it as your application grows. The main idea is simple: define design decisions once and let your components reuse them.

Contact us

Aarav Sharma

Aarav Sharma

Lead Software Engineer

Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.

Related articles

Let's Collaborate

Tell us about your project and we'll come back with a plan, a timeline, and a quote.

Project Type

Budget

Task Message

Your Contacts