Skip to content

FISYCO

Public beta

Token modes and a dark theme without duplication

6 min read

Engineering
One card of token names feeding a light panel and a dark panel, with the same names on both

A dark theme often starts as a copy. Someone duplicates the colour file, renames it dark.css, and edits the values. It works until the next change: a new token lands in one file and not the other, and the dark theme quietly falls behind. Token modes solve this by keeping one set of names and switching only the values.

This note shows how to build dark mode design tokens without duplication: how Figma models modes, what the DTCG format now says about themes, and three ways to ship modes as CSS.

Key takeaways

  • A theme is a second set of values for the same names, not a second set of tokens. Components keep reading color-bg-surface whatever the theme.
  • Only the semantic tier changes between light and dark. Primitives stay one palette, and component code doesn't know which mode is active.
  • Figma variable modes and the DTCG Resolver Module model this the same way: one token, one value per context.
  • In CSS, ship modes as attribute blocks, as separate files, or with light-dark(). Each fits a different setup.

One set of names, two sets of values

A mode is a context in which the same token resolves to a different value. Light and dark are the usual pair; brands, densities and platforms work the same way. An illustrative semantic set:

The names are identical in both columns. Only the references change, and they point into one shared palette. That's what "without duplication" means in practice: there's no dark-color-bg-surface, and no component that has to choose between two names.

This only works if tokens are split into tiers. Primitives such as gray.900 hold raw values and never change per mode. Semantic tokens such as color-bg-surface name a purpose, and they're the ones that switch. Our guide to naming design tokens explains the tiers in detail.

The large public systems describe the same split. GitHub Primer notes that "by inverting the scales, light and dark themes are able to share many of the same functional color tokens without custom overrides" (Primer). Atlassian puts it more briefly: "Themes are how we switch color schemes and styles everywhere using a single set of tokens" (Atlassian).

Modes in Figma

Figma builds modes into variables. Its help centre says "Variable modes allow us to represent different contexts of our designs without needing to create multiple frames for every context" (Figma).

A collection has one column per mode, and each variable has a value in every column. A frame picks its mode with Apply variable mode. When it's set to Auto, the frame inherits the mode from its parent, or falls back to the collection's default.

Two habits keep Figma modes easy to export:

  • Put modes on the semantic collection. Keep primitives in a single-mode collection, and let semantic variables alias them per mode. The export then carries references, not copied hex values.
  • Name modes for code. "Light" and "Dark" become file names and selectors later. "Mode 1" typically becomes something like mode-1, which tells a developer nothing. We learned this from our own exporter: it once named per-mode files by Figma's internal mode ID, which looks like 1:0. That gave files such as tokens.1:0.css, and a colon isn't a valid file name character on Windows. Files are now named after the mode.

Modes in the DTCG format

The DTCG format reached its 2025.10 release, and themes live in a separate Resolver Module published the same day. A resolver document lists sets of token files and modifiers. A modifier provides "conditional inclusion via the contexts map", and resolutionOrder says which sets and modifiers apply in which order (DTCG Resolver Module).

A theme modifier looks like this, simplified from the specification's own example:

The files that each context points to hold the same token names with different values. That's the same model as a Figma collection, written down as a standard.

Three ways to ship modes as CSS

Once the values exist, the question is how the browser switches between them. There are three common options.

1. Attribute blocks in one file

Every theme lives in one stylesheet. The default values sit in :root, and each mode overrides them under an attribute selector:

Switching the theme means setting data-theme="dark" on <html> or on any subtree. That makes it the most flexible option: a dark sidebar inside a light page is one attribute. To follow the operating system, set the attribute from prefers-color-scheme in a small script, or duplicate the block inside that media query.

2. One file per mode

Each mode gets its own stylesheet, such as tokens.light.css and tokens.dark.css. You load one and swap it, or load both behind media queries:

This suits apps with many modes, such as several brands, where shipping every brand to every user would be wasteful. When the files are generated, they can't drift apart the way a hand-copied dark.css does.

3. light-dark()

The newest option keeps both values in one declaration. MDN says light-dark() "accepts two colors or two images and returns a color or an image based on the active color scheme, without needing a prefers-color-scheme media feature". It needs color-scheme: light dark on the element or root. It's been Baseline for colours since May 2024; image arguments are newer, so check support before relying on them (MDN).

It's compact, and the browser handles the switch. It only covers two schemes, though, and only colours and images. For brands or density modes you still need one of the first two options.

Keeping modes from drifting

Generated modes can't fall out of step by copy and paste, but they can still diverge from the design.

  • Check completeness. Every semantic token needs a value in every mode. With attribute blocks, a missing dark value falls back to the :root value and shows up as a bright panel at night.
  • Keep literals out of modes. A mode value should reference a primitive. A hex value typed into the dark column bypasses the palette, just like a hex value in a component.
  • Ship mode changes as pull requests. Match variables by their Figma IDs, not their names, so a rename shows up as a rename rather than a deletion plus an addition. When a mode value changes in Figma, the code should get the same change in a pull request, not at the next manual export. Our note on Style Dictionary in practice covers the build side.

Where FISYCO fits

We build FISYCO to deliver modes from Figma variables as code. A tokens library exports each mode in the format you choose. With inline modes, tokens.css has the default values in :root and one [data-theme="…"] block per mode, named after the Figma mode. With separate files, each mode gets its own tokens.<mode>.css, and tokens.<mode>.scss if you export SCSS. The DTCG export writes one design-tokens.<mode>.tokens.json per mode; it doesn't write a resolver document yet.

Aliases stay aliases, so a dark value that points to gray.900 arrives as var(--…), not as a copied hex value. Changes come as a pull request when you publish the library in Figma, or on demand.

Export your Figma modes as tokens, and keep light and dark in one set of names.
#Design tokens#Figma
KK

Written by

Kirill Karpov

Founder, FISYCO

Kirill Karpov is the founder of FISYCO, a design system assurance tool that keeps Figma and production code in sync.

Get new articles by email

Design systems, tokens and design-to-code delivery. No spam, and you can leave in one click.

By subscribing you agree to get blog emails from FISYCO. Details are in the privacy policy.

Keep your design system and your code in sync

FISYCO connects Figma libraries and your codebase, finds drift and opens the fix as a pull request.

Start for free

Related articles

5 min read

Engineering

Finding hard-coded colours in a React codebase

Every hex value in a component opts it out of the design system. Search commands that find them in minutes, the false alarms to remove, and how to keep them out.

5 min read

Engineering

An SVG sprite from Figma, and when it beats components

One file of symbols instead of one component per icon: when an SVG sprite is the better choice, how to use it in React, and the gotchas that trip people up.

7 min read

Engineering

Figma icons to React components, end to end

From an icon component in Figma to a merged pull request: every step, what it produces, and the three delivery formats with real generated output.