SurveyStream

Documentdocs/tokens/README.md

SurveyStream — design tokens

Four artefacts, one source. Generated from color-system/tokens.json and typography/type-tokens.json, so they cannot drift from the specs they encode.

FileFormatConsume with
design-tokens.jsonW3C DTCGStyle Dictionary 4+, Tokens Studio, Figma Variables
theme.cssCSS custom propertiesAny stack. Import once, before your framework’s base layer.
tailwind.config.jsTailwind v3content + theme.extend
tailwind-v4.cssTailwind v4 @theme@import after theme.css

202 tokens · 119 aliases, all resolving · 46 primitives · 27 semantic roles per theme.

Quick start

/* 1. tokens first — they define the variables everything else reads */
@import "./tokens/theme.css";

/* 2a. Tailwind v4 */
@import "tailwindcss";
@import "./tokens/tailwind-v4.css";
// 2b. Tailwind v3
module.exports = require('./tokens/tailwind.config.js');

Then write ordinary utilities — they follow the theme with no dark: prefix, because the semantic variables are re-declared per theme rather than duplicated per utility:

<div class="bg-canvas text-primary rounded-lg shadow-md p-6">
  <h1 class="font-display text-h1">Turn feedback into value</h1>
  <p class="text-body text-muted">Body copy on a 4px grid.</p>
  <span class="bg-fill-payout text-on-accent border-hairline border-fill-payout-edge rounded-full px-3">
    Payout cleared
  </span>
</div>

Architecture

Three layers. Components may only reference layer 3.

primitive              →  semantic (per theme)  →  utility / component
color.deepwater.500    →  theme.light.color.action.interactive  →  bg-interactive
color.stream.500       →  theme.dark.color.action.interactive   →  bg-interactive

Every semantic token is an alias, never a literal — the generator throws if a semantic value has no primitive to point at. That rule is why color.slate.* exists: the dark UI’s structural values (dividers, disabled text, the sunken surface) had been authored straight into the semantic layer. A semantic token that cannot resolve to a primitive is not a token, it is a hard-coded value wearing a name.

// theme.light.color.text.primary  →  {color.deepwater.950}   #04171F
// theme.dark.color.text.primary   →  {color.slate.100}       #EDF3F4

Theming

Dark mode is applied by any of three mechanisms, and theme.css handles all three because a real product needs all three:

MechanismSelectorUse
OS preference@media (prefers-color-scheme: dark)The default for a visitor who has never chosen
Explicit choice[data-theme="dark"] / [data-theme="light"]An in-product toggle
Framework class.dark / .lightTailwind’s darkMode: 'class'

The media query is guarded — :root:not([data-theme="light"]):not(.light) — so an explicit light choice still wins over a dark OS. Never declare a colour only inside a media or [data-theme] block: the un-stamped state is the one most visitors are in, and a colour defined only in the stamped state simply will not apply to them.

tailwind.config.js sets darkMode: ['class', '[data-theme="dark"]'] so the dark: variant also honours the attribute, for the cases where you do want to branch explicitly.

Naming: why the CSS variable and the utility differ

CSS variables keep their property prefix because that is what reads correctly in raw CSS:

background: var(--color-bg-canvas);
color: var(--color-text-primary);

Tailwind must not repeat the prefix, or you end up typing text-text-primary. In v3 the fix is per-property theme keys (backgroundColor, textColor, borderColor); in v4, which has a single --color-* namespace, the prefix is dropped in the @theme mapping. Either way the utility reads as bg-canvas and text-primary.

Fills and feedback keep their prefix in both — success alone is ambiguous between the text colour and the solid fill, and those are genuinely different values.

Token sets

Colour

46 primitives across deepwater, stream, pulse, payout, neutral, slate, success, warn, error, base. 27 semantic roles per theme, grouped bg · text · border · action · feedback · fill.

Primitive utilities do not follow the theme. bg-stream-500 is a literal. Reach for those only when a colour must be pinned regardless of mode — a specimen plate, a fixed piece of artwork.

Typography

Ten steps on a 1.200 minor third from a 16px base, eight on-scale and two flagged off-scale. Emitted three ways: individual fontSize / lineHeight / letterSpacing tokens, DTCG composite typography tokens, and Tailwind fontSize tuples so text-h1 carries its leading and tracking automatically.

Spacing

A 4px grid — the same grid the type system snaps line-heights to, which is why the two sit together without half-pixel drift. The built pages had accumulated 6 / 7 / 9 / 18 / 26 / 30px values; those are rationalised here rather than enshrined.

Radii

Rationalised from what shipped (4 / 9 / 12 / 14 / 18 / 999) into xs sm md lg xl 2xl 3xl full.

Elevation

Five steps, different values per theme. This is deliberate: in light mode lift is carried by shadow, but in dark mode shadow is nearly invisible against a dark ground, so the dark set is heavier and the surfaces themselves step up in lightness. A single shadow scale would make dark mode look flat.

Motion

The most lightly-evidenced set here, and worth saying so. 180ms is the only UI transition the system actually shipped; the rest of the scale is derived from it, and 60s is the pattern drift cycle. Five easing curves and five composite transition tokens are provided, but there is no motion system yet — no choreography, no documented entrance/exit pairing per component. Treat these as sound defaults, not as a specification.

theme.css ships a prefers-reduced-motion block that collapses all animation and transition durations to 1ms.

Spec compliance notes

  • Dimensions are strings ("16px"), not the { value, unit } object form in the current DTCG editor’s draft. Strings are what Style Dictionary, Tokens Studio and Figma Variables actually consume today. If your pipeline wants the object form, one transform over $type: "dimension" converts the whole file.
  • $type is present on every token — 202 of 202.
  • Composite types used: typography, shadow, transition, cubicBezier, fontFamily, fontWeight, duration, dimension, color.
  • DTCG has no standardised theming mechanism yet. Themes here are a theme.light / theme.dark group whose leaves are aliases into the primitives — portable, and trivially splittable into per-theme files if your tooling prefers that.

Verification

Regenerating runs these checks and fails loudly:

CheckResult
Every {alias} resolves119 / 119
Every token has $type202 / 202
Light and dark semantic sets are identical in shape
Every semantic value resolves to a primitive✓ (throws otherwise)
slate ramp is monotonically darkening
CSS braces balanced, no undefined var()
Tailwind v3 config compiles and emits every utility

The last one is a real compile, not a lint: the config is run through Tailwind against a fixture using bg-canvas, text-h1, shadow-md, border-fill-payout-edge, duration-base, ease-standard and the rest, and each utility is confirmed in the output.

Relationship to the other files

color-system/tokens.json and typography/type-tokens.json remain the domain sources — they carry the contrast validation and the font evidence that justify the values. This folder is the consolidated export that adds spacing, radii, elevation and motion, and is what application code should consume. Change a value in the domain source, regenerate, and everything downstream follows.

BRAND-GUIDELINES.md is generated from the same domain sources, so the document and the tokens cannot disagree.