---
title: "Feature: Design tokens, with light and dark appearance"
manual: "Frontend Theme for Extension Development"
version: "main"
source: "Changelog/2.0/Feature-DesignTokens.rst"
modified: "2026-09-17T04:52:50+00:00"
---

# Feature: Design tokens, with light and dark appearance

## Description

The stylesheet is built from a documented set of **design tokens** covering
typography, colour, spacing, border radius, borders, focus, controls, stacking,
motion and layout width. Every one of them is a CSS custom property.

Every colour is declared **once** and carries both appearances, using the CSS
`light-dark()` function:

```css
:root {
    color-scheme: light dark;
    --theme-color-background: light-dark(#ffffff, #0f1319);
}
```

## Appearance

The theme follows the **appearance the visitor asked for**. The operating
system decides by default, and an explicit request on the root element
overrules it in both directions:

```html
<html data-theme="dark">
<html data-theme="light">
```

```css
:root                     { color-scheme: light dark; }
:root[data-theme='light'] { color-scheme: light; }
:root[data-theme='dark']  { color-scheme: dark; }
```

That is the whole mechanism. It works because `light-dark()` resolves
against the *used value* of `color-scheme`, so switching one property
switches every colour. It does **not** work by influencing what
`prefers-color-scheme` matches, which is tied to the operating system and
cannot be changed from CSS.

Setting `color-scheme` per appearance also makes form controls, scrollbars
and the canvas follow along.

No JavaScript ships with the extension - the attribute is there for whoever
wants to build a switch.

## Palettes

Four alternate palettes ship alongside the neutral default. A palette varies
**accents only** \- `primary`, `secondary`, their hover states and the
focus ring - while neutrals, semantic colour, spacing, radius and typography
stay shared:

```html
<html data-palette="ocean">
```

Available: `ember`, `ocean`, `moss`, `violet`. Omitting
the attribute selects the neutral default.

For a theme whose purpose is extension development, the palettes are a **test
surface** rather than decoration: an extension that renders correctly across
every palette in both appearances is one that is not hardcoding colour.

## Re-theming without a build

Custom properties survive compilation, so a site package can re-theme the
extension from its own CSS without rebuilding the SCSS:

```css
:root {
    --theme-color-primary: light-dark(#7b1fa2, #ce93d8);
    --theme-font-family-sans: 'Mulish', system-ui, sans-serif;
}
```

## Impact

The palette is deliberately **neutral**. This is a theme for extension
development, and its job is to make document structure legible without biasing
the design of the extension being built against it.

Every colour was checked for contrast against both the background and the
surface of its own appearance: body text clears 4.5:1 and any border that
delimits a control clears 3:1. Semantic colour carries three tokens per meaning
\- the accent for text and borders, `--theme-color-on-*` for a foreground on
a solid fill, and `--theme-color-*-surface` for the soft tint an alert sits
on - because one value cannot do all three jobs.

The design is **flat**. There are no elevation tokens, and the only shadow is
the focus ring, because a visible focus indicator is a requirement rather than
decoration. It is split into `--theme-focus-ring-color` and a composite
built around it, because `light-dark()` takes colours and not shadows.

> [!NOTE]
> `light-dark()` is supported by Firefox 120, Chrome and Edge 123, and
> Safari 17.5 - Baseline since May 2024.

> [!NOTE]
> The token names changed while this was still unreleased, which makes it a
> rename rather than a breaking change: no version had shipped at that point.
>
> | Previously | Now |
> | --- | --- |
> | `--theme-color-text` | `--theme-color-text-primary` |
> | `--theme-color-accent` | `--theme-color-primary` |
> | `--theme-font-family-base` | `--theme-font-family-sans` |
> | `--theme-font-size-base` | `--theme-font-size-md` |
> | `--theme-spacing-xs` … `--theme-spacing-xl` | `--theme-space-1` … `--theme-space-8` |
>
> The spacing scale is now a **5px** grid - 5, 10, 15, 20, 25, 30, 40, 60 -
> rather than the doubling scale it replaced.
