DocsBack to homepage

Start Here

  • Getting Started
  • Key Concepts

Design Tokens

  • Token Types
  • Token Modes
  • Token Enforcement
  • Deprecated Tokens
  • Quality and Accessibility

Components

  • Component Builder
  • Composition Rules

Publishing

  • Publishing
  • Docs Mode
  • Changelog Notifications
  • Notifications and Alerts

Integrations

CLI & Data

  • CLI Reference
  • CLI Configuration
  • Import Formats
  • Importing Tokens
  • Export Formats

Tooling

  • Studio AI Assistant
  • Figma Plugin
  • API Reference
  • Webhooks

Account & Billing

  • Audit Log
  • Security and Access
  • Account Security
  • Pricing and Payments

Documentation

Token Modes

Modes let you define multiple value sets for the same token: light and dark themes, brand variants, or regional overrides. At runtime, the correct set is applied based on context.

What are modes?

A mode is a named set of overrides for your semantic tokens. Instead of defining a separate token for each theme, you define a single semantic token (for example, color.surface.default) and give it a different value per mode.

The token name stays constant. Only the resolved value changes when you switch modes. This keeps your component code free of conditional logic like theme === 'dark' ? darkColor : lightColor.

Defining modes

Modes are created in Studio under Tokens > Modes. Click Add mode and give it a name. Common modes include light, dark, and brand variants like brand-a or brand-b.

For each semantic token, you can set a value per mode. Tokens without a mode-specific value fall back to the base value defined in the semantic layer.

In the exported token JSON, each mode maps to its own $extensions.mode block following the W3C DTCG format.

Runtime resolution

Modes are applied as CSS custom property overrides. The generated output includes a set of CSS variables for each mode. You activate a mode by adding a data attribute to any DOM element:

<!-- Activate dark mode on the root -->
<html data-mode="dark">

<!-- Or scope it to a subtree -->
<section data-mode="brand-a">
  <!-- Tokens inside resolve to the brand-a values -->
</section>

The CSS output generated by reframe sync includes a selector for each mode:

:root {
  --acme-color-surface-default: oklch(0.99 0.004 250);
}

[data-mode="dark"] {
  --acme-color-surface-default: oklch(0.12 0.01 250);
}

[data-mode="brand-a"] {
  --acme-color-surface-default: oklch(0.97 0.004 50);
}

Scoped mode application works because CSS custom properties inherit down the DOM tree. Setting data-mode on a subtree re-defines the variables for that subtree only.

Using modes with CLI sync

When you run reframe sync, the generated CSS file includes all modes automatically. No extra flags are required.

reframe sync

To generate the token file with only specific modes included, use the --modes flag:

reframe sync --modes light,dark

The output file path and format are configured in reframe.config.ts. See the CLI Reference for all available options.

Default mode and fallback

The first mode you create becomes the default. Its values are written to :root (no data attribute required). All other modes override on top of it.

A token with no mode override falls back to its base semantic value. The resolution order is: base → default mode → active mode override.

To change the default mode, go to Tokens > Modes and use the context menu on any mode to set it as the default.

Compare two modes side by side in the canvas

The component canvas supports a split view so you can render the same component with two different mode configurations at once. This makes it easy to compare light and dark themes, brand variants, or any other mode combination without switching back and forth.

Enabling split view

Open a component in the canvas, then click the Compare modes button in the toolbar (the two-panel icon). The canvas splits into a left panel and a right panel, each showing the component with its own mode configuration.

Split view is only available in canvas view. If the state grid is active, switch back to canvas view first. The Compare modes button is disabled while the grid is active.

Per-panel mode pickers

When split view is on, two mode picker buttons appear in the toolbar: one for the left panel and one for the right. Each picker shows the active mode label (or Base when all modes are at their defaults). Click a picker to open the mode selection popover for that panel only. Changing one panel does not affect the other.

Shared component tree

Both panels display the same component tree. Selecting a node in either panel highlights it in the inspector, and edits made in one panel are reflected in both. The split view is purely a display aid: there is one component definition, two visual representations.

Applying modes in your framework

data-mode is a plain HTML attribute. Any framework that can write an attribute onto a DOM element can switch modes. The examples below show the most common patterns.

React

Set the attribute on document.documentElement inside a useEffect hook. The component below toggles dark mode on the document root.

components/ThemeToggle.tsxtsx
'use client';
import { useEffect, useState } from 'react';

export function ThemeToggle() {
  const [dark, setDark] = useState(false);

  useEffect(() => {
    document.documentElement.setAttribute('data-mode', dark ? 'dark' : 'light');
  }, [dark]);

  return (
    <button onClick={() => setDark((d) => !d)}>
      {dark ? 'Switch to light' : 'Switch to dark'}
    </button>
  );
}

For Next.js App Router, apply the initial mode on the server by reading a cookie, then write it to the <html> element directly:

app/layout.tsxtsx
import { cookies } from 'next/headers';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const mode = (await cookies()).get('mode')?.value ?? 'light';
  return (
    <html data-mode={mode} suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

The 'use client' directive is required on any component that reads or writes browser APIs. If you set data-mode on the server using a cookie, add suppressHydrationWarning to the html element to prevent a hydration mismatch.

To apply a brand variant to one section only, add data-mode directly to a wrapper element. Tokens inside that element resolve from the named mode. Everything outside stays on the default.

<div data-mode="brand-a">
  {/* Tokens here resolve to the brand-a values */}
  <HeroSection />
</div>

Vue

Use a ref and a watcher to write the attribute whenever the mode value changes.

components/ThemeToggle.vuetsx
<script setup>
import { ref, watch } from 'vue';

const mode = ref('light');

watch(mode, (value) => {
  document.documentElement.setAttribute('data-mode', value);
});
</script>

<template>
  <button @click="mode = mode === 'dark' ? 'light' : 'dark'">
    {{ mode === 'dark' ? 'Switch to light' : 'Switch to dark' }}
  </button>
</template>

In Nuxt, access document inside onMounted to avoid running this code during server-side rendering. useHead does not write data attributes directly, so onMounted is the right place for this.

Wrap any element with data-mode to scope a mode to that subtree without affecting the rest of the page.

<div data-mode="brand-a">
  <!-- Tokens inside resolve to the brand-a values -->
  <HeroSection />
</div>

Svelte

In Svelte 5, use $effect to write the attribute reactively. In Svelte 4, use a reactive statement.

ThemeToggle.sveltetsx
<script>
  // Svelte 5
  let dark = $state(false);

  $effect(() => {
    document.documentElement.setAttribute('data-mode', dark ? 'dark' : 'light');
  });

  // Svelte 4 equivalent:
  // let dark = false;
  // $: document.documentElement.setAttribute('data-mode', dark ? 'dark' : 'light');
</script>

<button onclick={() => dark = !dark}>
  {dark ? 'Switch to light' : 'Switch to dark'}
</button>

Place this inside onMount in SvelteKit. Svelte runs reactive statements on the client by default, but onMount makes the intent explicit and avoids any SSR edge cases.

Angular

Create a ThemeService that writes the attribute on document.documentElement. Inject DOCUMENT from @angular/common rather than referencing the global document directly. This keeps the service compatible with Angular Universal (SSR).

theme.service.tstsx
import { inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private doc = inject(DOCUMENT);

  setMode(mode: string) {
    this.doc.documentElement.setAttribute('data-mode', mode);
  }
}

To scope a mode to a component's host element, call this.el.nativeElement.setAttribute('data-mode', value) inside the component, injecting ElementRef.

Common setup patterns

Most design systems need at least two behaviors: auto-detecting the OS preference and persisting the user's explicit choice. The patterns below cover both, plus a multi-axis brand variant setup.

Auto-detecting dark mode

Read prefers-color-scheme on page load and update the attribute when the OS setting changes.

init-mode.jstsx
const mq = window.matchMedia('(prefers-color-scheme: dark)');

function applyMode(dark) {
  document.documentElement.setAttribute('data-mode', dark ? 'dark' : 'light');
}

applyMode(mq.matches);
mq.addEventListener('change', (e) => applyMode(e.matches));

This sets a baseline. If the user has chosen a mode explicitly, their stored preference should take priority (see below).

Persisting the user's choice

Store the user's choice in localStorage and restore it on the next visit. Initialize in a synchronous script in <head> to avoid a flash of the wrong mode before JavaScript loads.

index.html (inside <head>)html
<script>
  (function () {
    var stored = localStorage.getItem('mode');
    var auto = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    document.documentElement.setAttribute('data-mode', stored ?? auto);
  })();
</script>

The synchronous script runs before any framework hydration, so the correct mode is applied before the first paint.

Brand variant switcher

When modes represent orthogonal axes (for example, light/dark and a brand color), use separate mode namespaces published from Studio. Set both attributes on the same root element.

<html data-mode-theme="dark" data-mode-brand="brand-a">
  <!-- Tokens resolve from both the dark theme and brand-a variant -->
</html>

This requires the design system to be published with separate named mode groups. When running reframe sync, use the --modes flag to generate only the subsets your app needs. The CSS selectors must match the attribute names exactly.

Known limitations

The following behaviors are by design but can be surprising. Read through them before building a complex mode setup.

  • •Renaming a mode in Studio changes the CSS selector from [data-mode="old-name"] to [data-mode="new-name"]. Consumer code that still sets the old name gets no override and silently falls back to the default mode. After renaming, search the consumer codebase for the old attribute value and update every reference.
  • •An inner data-mode attribute overrides the outer one for all tokens in that subtree. There is no way to merge two modes on the same element. Only the innermost attribute takes effect.
  • •If the consumer wraps the generated CSS in a @layer, [data-mode] selectors may lose to unlayered rules due to specificity. Import the generated CSS outside any layer, or increase specificity at the import site.
  • •The data-mode attribute must be written to the HTML before the first paint to avoid a flash of default styles. Set it in a synchronous script in <head>, or apply it on the server using a cookie or session value.

Troubleshooting

Start by confirming the generated CSS file is imported at the app root and that the attribute name matches exactly what reframe sync outputs. Most mode issues come down to one of these.

Mode not applying at all

Check that the generated CSS file is imported at the app root. Open DevTools, select an element inside the mode scope, and look for [data-mode="dark"] (or whichever mode you set) in the Styles panel. If no matching rule appears, the file was not loaded. If a rule exists but has no effect, confirm the attribute name matches the CSS selector exactly. The match is case-sensitive.

Mode applies globally but not in a subtree

Verify data-mode is on an ancestor of the content you are testing, not a sibling. CSS custom property inheritance flows downward only. An attribute on a sibling element has no effect on the target element's resolved values.

Flash of wrong mode on load

The attribute is being set in JavaScript after hydration. Move the initial mode detection into a synchronous script in <head> so it runs before any rendering. In Next.js App Router, add suppressHydrationWarning to the <html> element when the attribute is set server-side to prevent a hydration mismatch.

Mode tokens look correct in Studio but wrong in the app

Run reframe sync again. Your local CSS file may be out of date. After syncing, open the generated file and compare the [data-mode] selector values against what Studio shows. If they match, clear your build cache and reload.

Mode works in development but not in production

Check that the generated CSS file is included in the production build. Some bundlers skip CSS that appears unused. Import the file unconditionally in the root layout, not inside a conditional branch.

Migrating from hardcoded theme switching

Teams often start by duplicating token files or toggling CSS classes to switch themes. Migrating to modes removes the duplication and keeps all values in one place.

  1. 1.Map your existing overrides to modes. If you have a dark CSS class that overrides custom properties, create a dark mode in Studio and enter those same values into the mode columns. Publish when done.
  2. 2.Replace class toggles with data-mode. Change element.classList.toggle('dark') to element.setAttribute('data-mode', 'dark'). Remove the old class-based CSS override rules.
  3. 3.Delete the duplicated token files. If you had a tokens-dark.css alongside tokens.css, delete it. All mode values now live in the single published file, scoped by [data-mode] selectors.

If you used prefers-color-scheme media queries directly in your token file, copy those values into a mode in Studio and remove the media queries. The OS preference detection belongs in JavaScript. See Common setup patterns above for the recommended approach.