Colour

Colour tokens give every surface, text, border, icon and status in Nuton a named role. Use the semantic tokens in product code; the palette underneath is what they point to.

  • Beta
  • Accessibility
  • Figma Mapped and Foundation collections
  • packages/tokens/src/figma/snapshot.json, src/interim/semantic.json

Beta. Semantic colours come from the Figma Mapped collection and the palette from Foundation. The three action-primary tokens (D13) and the focus-ring colour (C5) are interim, waiting for Figma variables. Stable needs the code-only dark values (C69) approved and a Dark mode added in Figma (D8, TD-11, FC-38), a design decision on the input-border contrast (C7), and the remote-library links re-pointed to local Foundation (TD-02).

Overview

Colour in Nuton has two tiers. Semantic tokens name a job (secondary text, a soft border, an error fill); the palette holds the raw steps they point to. Components and product code use semantic tokens only (D3), so a change of value, or a future dark theme, happens in one place.

Semantic colour roles
RolePrefixUse forTokens
Background--nu-bg-*Page, card and control fills.
Text--nu-text-*Text, by emphasis (D7).
Stroke--nu-stroke-*Borders and dividers.
Icon--nu-icon-*Icon fills, matched to the text beside them.
Action--nu-action-*The neutral main action (D13).
State--nu-state-*Status, feedback and the interactive blue.
Overlay--nu-overlay-*Scrims behind dialogs and sheets.
Illustration--nu-illustration-*Greys for empty-state artwork (TD-08).

Swatches render the live CSS variables. The State row shows each family's Base step; every family also has Dark, Light and Lighter.

Roles on a real surface

  • Photosynthesis basicsBiology 101 · 12 minCompleted
  • Cell divisionBiology 101 · 18 minIn progress
  • Quiz: the cellBiology 101 · 10 questions

Dark mode

Nuton has three themes: Light, Dark and System (follows the device). Pick one by setting data-theme on <html>, or on any element to theme just that part. With no attribute you get Light. The Theme toggle sets it for the whole page and remembers the choice.

tsx
<html data-theme="system">   <!-- follows the device -->
<html data-theme="dark">
<section data-theme="dark">  <!-- one always-dark area -->

The dark values are code-only (D8, C69): Figma has one colour mode, so each palette ramp is reversed (Gray-50 swaps with Gray-950, and so on for every hue) and the semantic tokens keep pointing at the same palette names. Components need no dark rules. Shadows get stronger, and translucent black and white tints swap. Every required text and non-text pair is re-checked in dark by npm run tokens:check. Dark needs design approval, and a Dark mode on the Mapped collection (FC-38), before Stable.

TokenLightDark
--nu-bg-white #ffffff #171717
--nu-bg-weak-50 #f7f7f7 #1c1c1c
--nu-bg-soft-100 #f5f5f5 #262626
--nu-bg-strong-950 #171717 #ffffff
--nu-text-strong-950 #171717 #ffffff
--nu-text-sub-600 #5c5c5c #d1d1d1
--nu-stroke-soft-200 #ebebeb #333333
--nu-state-primary-base #3559e9 #97baff
--nu-state-error-base #e93544 #ff6875
--nu-state-success-base #1daf61 #3ee089

Raw colours (hex, or palette steps used for fixed looks) do not change with the theme. For something that must stay dark, put data-theme="dark" on it and use ordinary semantic tokens inside.

Tokens

Every table is generated from @nuton/tokens. Value shows the alias the token points to and the resolved colour; Source shows where it's authored. Figma names are kept verbatim, numeric suffix included: Text/Sub-600 becomes --nu-text-sub-600 (D3).

Background

Background colour tokens
PreviewTokenValueSource
--nu-bg-strong-950→ gray-950#171717FigmaMapped/Bg/Strong-950
--nu-bg-surface-800→ gray-800#262626FigmaMapped/Bg/Surface-800
--nu-bg-sub-300→ gray-300#d1d1d1FigmaMapped/Bg/Sub-300
--nu-bg-soft-200→ gray-200#ebebebFigmaMapped/Bg/Soft-200
--nu-bg-soft-100→ gray-100#f5f5f5FigmaMapped/Bg/Soft-100
--nu-bg-weak-50→ gray-50#f7f7f7FigmaMapped/Bg/Weak-50
--nu-bg-white→ gray-0#ffffffFigmaMapped/Bg/White

White is the default page and card fill; Weak-50 and Soft-100/200 are quieter surfaces and hover fills. Strong-950 and Surface-800 are the dark fills behind the main action.

Text

Text colour tokens
PreviewTokenValueSource
--nu-text-strong-950→ gray-950#171717FigmaMapped/Text/Strong-950
--nu-text-sub-600→ gray-600#5c5c5cFigmaMapped/Text/Sub-600
--nu-text-soft-400→ gray-400#a4a4a4FigmaMapped/Text/Soft-400
--nu-text-disabled-300→ gray-300#d1d1d1FigmaMapped/Text/Disabled-300
--nu-text-white→ gray-0#ffffffFigmaMapped/Text/White

D7: Strong-950 for primary text, Sub-600 for secondary text, hints and placeholders. Soft-400 is decorative only: it measures 2.49:1 on white (C1). Disabled text uses Disabled-500 (gray-500, 4.23:1 on white), darkened from Figma's Disabled-300 so it stays readable (C8).

Stroke

Stroke colour tokens
PreviewTokenValueSource
--nu-stroke-strong-950→ gray-950#171717FigmaMapped/Stroke/Strong-950
--nu-stroke-sub-300→ gray-300#d1d1d1FigmaMapped/Stroke/Sub-300
--nu-stroke-soft-200→ gray-200#ebebebFigmaMapped/Stroke/Soft-200
--nu-stroke-white→ gray-0#ffffffFigmaMapped/Stroke/White

Soft-200 for dividers and card borders. Sub-300 is the default input border, which measures 1.53:1 on white (C7). Strong-950 marks hover and focus.

Icon

Icon colour tokens
PreviewTokenValueSource
--nu-icon-strong-950→ gray-950#171717FigmaMapped/Icon/Strong-950
--nu-icon-sub-600→ gray-600#5c5c5cFigmaMapped/Icon/Sub-600
--nu-icon-soft-400→ gray-400#a4a4a4FigmaMapped/Icon/Soft-400
--nu-icon-disabled-300→ gray-300#d1d1d1FigmaMapped/Icon/Disabled-300
--nu-icon-white→ gray-0#ffffffFigmaMapped/Icon/White

Match the icon to the text it sits with: Strong-950 beside primary text, Sub-600 beside secondary text. Soft-400 suits decorative icons that are hidden from assistive tech.

Action

Action colour tokens
PreviewTokenValueSource
--nu-action-primary-bg→ bg-strong-950#171717Interim
--nu-action-primary-bg-hover→ bg-surface-800#262626Interim
--nu-action-primary-fg→ text-white#ffffffInterim

Action is not state-primary (D13)

Nuton's main action is the neutral black pill: --nu-action-primary-*. The blue --nu-state-primary-* family is a state colour for interactive emphasis and links, not “the most important button”. The bare primary name is retired. The action tokens are interim until Figma has an Action group; their values copy Figma's neutral filled button.

State

10 families (faded, primary, purple, success, error, warning, pending, feature, teal, verified), each with four steps. In @nuton/ui today, Lighter is a tinted background, Light is its hover, and Base is the solid colour for fills, icons, dots and labels. No component uses Dark yet; it reads as text on Lighter (Success Dark on Success Lighter is 6.53:1).

State colour tokens
PreviewTokenValueSource
--nu-state-faded-dark→ gray-800#262626FigmaMapped/State/Faded/Dark
--nu-state-faded-base→ gray-500#7b7b7bFigmaMapped/State/Faded/Base
--nu-state-faded-light→ gray-200#ebebebFigmaMapped/State/Faded/Light
--nu-state-faded-lighter→ gray-100#f5f5f5FigmaMapped/State/Faded/Lighter
--nu-state-primary-dark→ blue-900#182f8bFigmaMapped/State/Primary/Dark
--nu-state-primary-base→ blue-600#3559e9FigmaMapped/State/Primary/Base
--nu-state-primary-light→ blue-200#c0d5ffFigmaMapped/State/Primary/Light
--nu-state-primary-lighter→ blue-50#ebf1ffFigmaMapped/State/Primary/Lighter
--nu-state-purple-dark→ purple-950#2b1164FigmaMapped/State/Purple/Dark
--nu-state-purple-base→ purple-500#7d52f4FigmaMapped/State/Purple/Base
--nu-state-purple-light→ purple-200#dbd6feFigmaMapped/State/Purple/Light
--nu-state-purple-lighter→ purple-50#f5f3ffFigmaMapped/State/Purple/Lighter
--nu-state-success-dark→ green-900#16643bFigmaMapped/State/Success/Dark
--nu-state-success-base→ green-600#1daf61FigmaMapped/State/Success/Base
--nu-state-success-light→ green-200#c2f5daFigmaMapped/State/Success/Light
--nu-state-success-lighter→ green-50#e0faecFigmaMapped/State/Success/Lighter
--nu-state-error-dark→ red-900#8b1822FigmaMapped/State/Error/Dark
--nu-state-error-base→ red-600#e93544FigmaMapped/State/Error/Base
--nu-state-error-light→ red-200#ffc0c5FigmaMapped/State/Error/Light
--nu-state-error-lighter→ red-50#ffebecFigmaMapped/State/Error/Lighter
--nu-state-warning-dark→ orange-950#71330aFigmaMapped/State/Warning/Dark
--nu-state-warning-base→ orange-600#e16614FigmaMapped/State/Warning/Base
--nu-state-warning-light→ orange-200#ffd9c0FigmaMapped/State/Warning/Light
--nu-state-warning-lighter→ orange-50#fff3ebFigmaMapped/State/Warning/Lighter
--nu-state-pending-dark→ yellow-950#624c18FigmaMapped/State/Pending/Dark
--nu-state-pending-base→ yellow-600#e6a819FigmaMapped/State/Pending/Base
--nu-state-pending-light→ yellow-200#ffecc0FigmaMapped/State/Pending/Light
--nu-state-pending-lighter→ yellow-50#fffaebFigmaMapped/State/Pending/Lighter
--nu-state-feature-dark→ pink-950#68123dFigmaMapped/State/Feature/Dark
--nu-state-feature-base→ pink-600#e9358fFigmaMapped/State/Feature/Base
--nu-state-feature-light→ pink-200#ffc0dfFigmaMapped/State/Feature/Light
--nu-state-feature-lighter→ pink-50#ffebf4FigmaMapped/State/Feature/Lighter
--nu-state-teal-dark→ teal-950#0b463eFigmaMapped/State/Teal/Dark
--nu-state-teal-base→ teal-700#178c7dFigmaMapped/State/Teal/Base
--nu-state-teal-light→ teal-200#c2f5eeFigmaMapped/State/Teal/Light
--nu-state-teal-lighter→ teal-50#e4fbf8FigmaMapped/State/Teal/Lighter
--nu-state-verified-dark→ sky-950#124b68FigmaMapped/State/Verified/Dark
--nu-state-verified-base→ sky-700#2597d0FigmaMapped/State/Verified/Base
--nu-state-verified-light→ sky-200#c0eaffFigmaMapped/State/Verified/Light
--nu-state-verified-lighter→ sky-100#d5f1ffFigmaMapped/State/Verified/Lighter

The palette step behind each step isn't uniform across families: Dark is 900 or 950, Base is 500, 600 or 700, and Verified Lighter is 100 (TD-07, design to confirm). Teal and Verified skip the Alias layer in Figma (TD-06). Purple and Feature may be renamed in Figma (TD-09).

Overlay

Overlay colour tokens
PreviewTokenValueSource
--nu-overlay-gray#3333333dFigmaMapped/overlay/overlay-gray
--nu-overlay-slate#2b303b3dFigmaMapped/overlay/overlay-slate

Literal values with alpha in Figma, not aliases. Figma's repeated group name (overlay/overlay-gray) is collapsed in code; the Figma rename is queued as TD-09.

Illustration

Illustration colour tokens
PreviewTokenValueSource
--nu-illustration-strong-400→ gray-400#a4a4a4FigmaMapped/illustration/strong-400
--nu-illustration-sub-300→ gray-300#d1d1d1FigmaMapped/illustration/sub-300
--nu-illustration-soft-200→ gray-200#ebebebFigmaMapped/illustration/soft-200
--nu-illustration-weak-100→ gray-100#f5f5f5FigmaMapped/illustration/weak-100
--nu-illustration-white-0→ gray-0#ffffffFigmaMapped/illustration/white-0

Lower-case in Figma, skips the Alias layer and has no bindings in the DS file. Proposed for deprecation unless another Nuton file uses it (TD-08, Q-04). Prefer Icon or Stroke tokens.

Global palette

Components use semantic tokens, not the palette (D3, D13)

The palette is a copy of the Figma Foundation collection. It isn't exposed as Tailwind utilities, so product code can't bypass the semantic layer. A few component tokens read it where Figma binds a palette step directly: the primary and error button hovers (Blue 700, Red 700) and Avatar persona colours (hue 200 and 950). If no semantic token fits, ask for one rather than reaching for a hue.

Gray palette tokens
PreviewTokenValueSource
--nu-gray-950#171717FigmaFoundation/neutral/gray/950
--nu-gray-900#1c1c1cFigmaFoundation/neutral/gray/900
--nu-gray-800#262626FigmaFoundation/neutral/gray/800
--nu-gray-700#333333FigmaFoundation/neutral/gray/700
--nu-gray-600#5c5c5cFigmaFoundation/neutral/gray/600
--nu-gray-500#7b7b7bFigmaFoundation/neutral/gray/500
--nu-gray-400#a4a4a4FigmaFoundation/neutral/gray/400
--nu-gray-300#d1d1d1FigmaFoundation/neutral/gray/300
--nu-gray-200#ebebebFigmaFoundation/neutral/gray/200
--nu-gray-100#f5f5f5FigmaFoundation/neutral/gray/100
--nu-gray-50#f7f7f7FigmaFoundation/neutral/gray/50
--nu-gray-0#ffffffFigmaFoundation/neutral/gray/0

Usage

Load tokens.css once, then reference CSS variables directly or through the Tailwind theme. Tailwind exposes semantic colours only, named after the Figma path, so the role appears twice: bg-bg-white, text-text-sub-600.

css
@import '@nuton/tokens/tokens.css';

.lesson-row {
  background: var(--nu-bg-white);
  border: 1px solid var(--nu-stroke-soft-200);
  color: var(--nu-text-strong-950);
}
.lesson-row:hover { background: var(--nu-bg-weak-50); }
.lesson-row__meta { color: var(--nu-text-sub-600); }
.lesson-row__status {
  background: var(--nu-state-success-lighter);
  color: var(--nu-state-success-dark);
}

Choosing a token

  • Pick the role first, then the emphasis. Fills use bg-, text uses text-, borders use stroke-, icons use icon-. Don't borrow across roles because two values match today; dark mode may split them (D8).
  • Main action: action-primary-*. Use state-primary-* only where the interactive blue is meant, such as links and selected controls (D13).
  • Status: a Lighter background with Dark text, or Base for icons and dots. White text passes on state-primary-base (5.59:1); Success, Pending, Warning, Error, Feature, Verified and Teal Base fail with white text (A2).
  • Secondary text: text-sub-600, including placeholders and hints. text-soft-400 is decorative only (D7, C1).
  • No raw values. Lint blocks hex colours and arbitrary Tailwind colours in @nuton/ui; the palette is CSS-only.

In use

Real @nuton/ui components, showing which semantic tokens they read.

Action and state colours on buttons

The neutral filled button is the main action; blue is the state colour (D13). Error buttons and error text use red-700, because Figma's state-error-base is 4.15:1 (C6, resolved 2026-10-14); see the Button page.

Text roles in a form

For example, Biology 101.

Figma draws placeholders in Soft-400; code uses Sub-600 so they pass 4.5:1 (D7, C1). The default border is Stroke/Sub-300 (C7). Error and disabled states are on the Inputs and fields page.

State colours as data series

  • Lessons completed
  • Quizzes taken
  • Flashcards reviewed

Chart colours map to State Base tokens, and every series keeps a text label so colour is never the only cue (WCAG 1.4.1).

Accessibility

Ratios below are computed at build time from the resolved token values, with the same WCAG formula as npm run tokens:check. Text needs 4.5:1; borders, focus rings and other UI parts need 3:1.

Contrast of semantic colour pairs
Foreground on backgroundUsed forRatioNeedsResult
text-strong-950 on bg-whiteBody text and headings17.93:14.5:1Pass
text-sub-600 on bg-whiteSecondary text, placeholders (D7, C1)6.69:14.5:1Pass
text-sub-600 on bg-weak-50Secondary text on weak surfaces6.24:14.5:1Pass
text-strong-950 on bg-soft-100Neutral lighter button label16.44:14.5:1Pass
action-primary-fg on action-primary-bgMain action label17.93:14.5:1Pass
action-primary-fg on action-primary-bg-hoverMain action label, hover15.13:14.5:1Pass
text-white on state-primary-basePrimary filled button label5.59:14.5:1Pass
state-primary-base on bg-whitePrimary stroke, ghost and link label5.59:14.5:1Pass
state-primary-base on state-primary-lighterPrimary lighter label4.94:14.5:1Pass
stroke-strong-950 on bg-whiteInput border on hover and focus17.93:13:1Pass
focus-ring-color on bg-whiteFocus ring colour (interim, C5)5.59:13:1Pass
text-soft-400 on bg-whiteSoft-400 used as text2.49:14.5:1Below, C1: decorative only
red-700 on bg-whiteError hint text; error stroke and ghost label (C6, red-700)5.26:14.5:1Pass
text-white on red-700Error filled button label (C6, red-700)5.26:14.5:1Pass
red-700 on state-error-lighterError lighter label (C6, red-700)4.59:14.5:1Pass
state-error-base on bg-whiteError borders and icons (non-text)4.15:13:1Pass
stroke-sub-300 on bg-whiteDefault input border, the only boundary cue1.53:13:1Below, C7
text-disabled-500 on bg-whiteDisabled text, labels and hints (C8, gray-500)4.23:13:1Pass
text-disabled-500 on bg-weak-50Disabled label on a weak fill (C8)3.95:13:1Pass
text-white on state-success-baseWhite text on a success fill2.85:14.5:1Below, A2: don’t use

Browser axe on the component pages reports the same failures; it measures the rendered disabled hint at 1.52:1 (docs/quality-strategy.md). Every failing pair is a Figma value implemented as specified, waiting on a design decision.

Text contrast

Strong-950 and Sub-600 pass on every Nuton surface. Never set meaningful text in Soft-400 or Disabled-300; disabled text uses Disabled-500 (C8). Error text uses Red 700 (5.26:1), not State/Error/Base (4.15:1); the base red stays for borders and icons (C6).

Non-text contrast

When a border is a control's only boundary, it needs 3:1. Stroke/Sub-300 doesn't reach it (C7). Hover and focus borders in Strong-950 pass. The interim focus-ring colour is 5.59:1 on white; Figma's focus effects are below 3:1 (C5). See Layout and responsive for focus rings.

Colour alone

Never use colour as the only signal (WCAG 1.4.1). Pair state colours with text or an icon: chart series have labels, errors have a message, Avatar status needs a label (C22).

Themes and forced colours

Only the light theme exists (D8). In Windows forced-colours mode the system palette replaces these tokens, so keep borders and text, not fills alone, carrying meaning.

Guidelines and related

Use semantic colour tokens for

  • Every fill, text, border and icon colour in product code and components.
  • Status and feedback, with State Lighter backgrounds and Dark text.
  • The main action, through action-primary-*.

Don't

  • Use palette tokens or hex values in product code.
  • Use state-primary blue to mean “most important action” (D13).
  • Put meaningful text in Soft-400, or white text on Success, Error or the other Base fills listed in A2.
  • Invent dark-mode values in code (D8).

Do and don't

DoMake the main action neutral. Black is Nuton's main-action colour; blue stays free to mean interactive emphasis.
Don'tDon't reach for blue to make an action stand out. state-primary is a state colour, not a priority level (D13).
Cell divisionBiology 101 · 18 min
DoUse Sub-600 for secondary text. Hierarchy comes from Strong-950 versus Sub-600, and both pass 4.5:1.
Cell divisionBiology 101 · 18 min
Don'tDon't colour plain text with the state blue. Blue text reads as a link, so people try to click it.
LessonsQuizzes
DoLabel every colour. A text label carries the meaning for people who can't tell the hues apart.
Don'tDon't rely on colour alone. Two dots with no labels leave the chart unreadable for many people (WCAG 1.4.1).