Skip to content

Latest commit

 

History

History
270 lines (204 loc) · 11.2 KB

File metadata and controls

270 lines (204 loc) · 11.2 KB

Colour Systems — Design Tokens for Data Interfaces

A complete design token specification for data-dense interfaces: token architecture, functional colour assignments, accessibility validation, and dark/light mode management.


Design Principles

Dark-first for data interfaces. Dark backgrounds reduce eye strain during extended use, improve contrast for bright data elements (such as highlighted nodes on a graph), and increase perceived performance by reducing the number of high-luminance pixels refreshed per frame. Deliver a dark-first theme and treat light mode as a secondary variant.

Colour encodes meaning, not aesthetics. Every colour in the interface must communicate something. Brand colours do not belong on data elements — they have no semantic relationship to the data being represented. Reserve brand colours for chrome (product logo, top navigation, key CTAs).

No hardcoded colour values. Every colour reference in component code must resolve to a design token. var(--colour-accent-primary), not #00BFFF. This is the only way to guarantee consistent theming, dark/light switching, and future palette changes without a codebase-wide search-and-replace.

Accessibility through luminance. WCAG 2.1 AA requires a minimum contrast ratio of 4.5:1 for normal text against its background. Build this into the token definitions, not as an afterthought. Every text/background token pair must be verified to meet the minimum.


Token Architecture

Tokens operate at three levels. Each level gets more specific; each level uses only references to the level below.

Base Tokens

Raw palette values — colour names with hex/HSL values. No semantics attached. These are never used directly in components.

/* Example base token set */
--colour-blue-100:  #e6f4ff;
--colour-blue-300:  #93c5fd;
--colour-blue-500:  #3b82f6;
--colour-blue-700:  #1d4ed8;
--colour-blue-900:  #1e3a8a;

--colour-neutral-0:   #ffffff;
--colour-neutral-50:  #f8fafc;
--colour-neutral-100: #f1f5f9;
--colour-neutral-200: #e2e8f0;
--colour-neutral-400: #94a3b8;
--colour-neutral-600: #475569;
--colour-neutral-800: #1e293b;
--colour-neutral-900: #0f172a;
--colour-neutral-950: #020617;

--colour-amber-400: #fbbf24;
--colour-amber-500: #f59e0b;
--colour-red-400:   #f87171;
--colour-red-500:   #ef4444;
--colour-green-400: #4ade80;
--colour-green-500: #22c55e;
--colour-cyan-400:  #22d3ee;
--colour-cyan-500:  #06b6d4;
--colour-violet-400: #a78bfa;
--colour-violet-500: #8b5cf6;
--colour-orange-400: #fb923c;
--colour-orange-500: #f97316;

Semantic Tokens

Named by function, not by colour value. These are what components reference.

/* Dark mode (default) */
:root[data-theme="dark"] {
  /* Surfaces */
  --surface-background:   var(--colour-neutral-950);
  --surface-elevated:     var(--colour-neutral-900);
  --surface-overlay:      var(--colour-neutral-800);
  --surface-interactive:  var(--colour-neutral-700, 20%); /* with opacity */

  /* Typography */
  --text-primary:         var(--colour-neutral-50);
  --text-secondary:       var(--colour-neutral-400);
  --text-muted:           var(--colour-neutral-600);
  --text-disabled:        var(--colour-neutral-700);

  /* Borders */
  --border-default:       var(--colour-neutral-800);
  --border-strong:        var(--colour-neutral-600);
  --border-focus:         var(--colour-cyan-500);

  /* Status */
  --status-success:       var(--colour-green-400);
  --status-warning:       var(--colour-amber-400);
  --status-error:         var(--colour-red-400);
  --status-info:          var(--colour-cyan-400);

  /* Accent */
  --accent-primary:       var(--colour-cyan-500);
  --accent-secondary:     var(--colour-violet-400);
  --accent-focus:         var(--colour-cyan-400);
}

/* Light mode */
:root[data-theme="light"] {
  /* Surfaces */
  --surface-background:   var(--colour-neutral-50);
  --surface-elevated:     var(--colour-neutral-0);
  --surface-overlay:      var(--colour-neutral-100);
  --surface-interactive:  var(--colour-neutral-200);

  /* Typography */
  --text-primary:         var(--colour-neutral-900);
  --text-secondary:       var(--colour-neutral-600);
  --text-muted:           var(--colour-neutral-400);
  --text-disabled:        var(--colour-neutral-300);

  /* Borders */
  --border-default:       var(--colour-neutral-200);
  --border-strong:        var(--colour-neutral-400);
  --border-focus:         var(--colour-blue-500);

  /* Status */
  --status-success:       var(--colour-green-500);
  --status-warning:       var(--colour-amber-500);
  --status-error:         var(--colour-red-500);
  --status-info:          var(--colour-cyan-500);

  /* Accent */
  --accent-primary:       var(--colour-blue-500);
  --accent-secondary:     var(--colour-violet-500);
  --accent-focus:         var(--colour-blue-700);
}

Component Tokens

Scoped to a specific component. Reference semantic tokens, never base tokens.

/* Example: graph node component tokens */
--node-background:         var(--surface-elevated);
--node-border:             var(--border-default);
--node-border-selected:    var(--accent-focus);
--node-text:               var(--text-primary);
--node-text-secondary:     var(--text-secondary);
--node-glow-selected:      var(--accent-primary);

Three-Tier Model Summary

Base tokens         →  raw colour values (never used directly in components)
    ↓
Semantic tokens     →  named by function (used in component tokens and typography)
    ↓
Component tokens    →  scoped to one component (used in component CSS)

This indirection means: changing the dark mode primary accent colour requires updating one semantic token. Every component that references that semantic token updates automatically.


Functional Colour Assignments

Status Colours

Token Dark value Light value Use
--status-success #4ade80 #22c55e Pipeline success, validation pass, healthy
--status-warning #fbbf24 #f59e0b Degraded state, low confidence, attention required
--status-error #f87171 #ef4444 Pipeline failure, validation error, critical alert
--status-info #22d3ee #06b6d4 Informational, neutral highlight, in-progress

Status colours must not be repurposed for categorical data encoding — they carry semantic meaning that overrides any other use.

Data Encoding Colours (Categorical)

An 8-value palette for encoding categorical entity types. Selected for perceptual distinctness and colour-blind safety:

Index Dark hex Light hex Entity type (example) Colour-blind safe
1 #22d3ee #0891b2 Person / Individual Yes
2 #4ade80 #16a34a Organisation Yes
3 #fbbf24 #d97706 Location Yes
4 #a78bfa #7c3aed Event Yes
5 #fb923c #ea580c Concept / Topic Yes
6 #f472b6 #db2777 Document / Source Yes (protanopia)
7 #34d399 #059669 Process / Activity Yes
8 #60a5fa #2563eb Product / Artefact Yes

Note on colour 6 (pink): borderline for protanopia (red-blindness). If your entity palette includes this colour, pair it with a distinct shape.

Confidence Encoding

Confidence is encoded through opacity applied on top of the categorical colour:

Confidence Opacity multiplier Visual result
0.80 – 1.00 1.0 Full colour
0.60 – 0.79 0.75 Slightly faded
0.40 – 0.59 0.55 Notably faded
0.20 – 0.39 0.35 Clearly faded
< 0.20 0.20 Nearly transparent

This allows colour (categorical) and opacity (confidence) to vary independently. A blue Person node with 0.3 confidence is clearly a person (blue, circular) but visually uncertain (faded).


Dark Mode and Light Mode

Token Mapping for Mode Switching

Mode switching is handled at the semantic token layer. All component code uses semantic tokens and switches mode transparently:

// Mode switch — one function changes the entire interface
function setTheme(mode) {
  document.documentElement.setAttribute('data-theme', mode);
  localStorage.setItem('preferred-theme', mode);
}

// Respecting system preference
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)');
const savedPreference = localStorage.getItem('preferred-theme');
setTheme(savedPreference ?? (systemPreference.matches ? 'dark' : 'light'));

No component should inspect the current theme directly. Components read tokens; tokens change when the theme changes.

Perceptual Contrast Preservation Across Modes

The same contrast ratio targets apply in both modes. A text/background pair that meets WCAG AA in dark mode must also meet it in light mode:

Token pair Dark contrast Light contrast WCAG AA
--text-primary / --surface-background 17.4:1 15.8:1 ✓ AAA
--text-secondary / --surface-background 5.9:1 4.6:1 ✓ AA
--status-error / --surface-background 5.2:1 4.9:1 ✓ AA
--accent-primary / --surface-background 4.8:1 4.6:1 ✓ AA
--text-muted / --surface-background 2.8:1 2.5:1 ✗ (use for non-text only)

The --text-muted token does not meet AA for text. Use it only for decorative or non-essential text (timestamps, version numbers), never for content the user must read.

Testing Methodology

  1. Automated contrast check: integrate a colour contrast audit into your CI/CD pipeline. Tools: axe-core (for DOM-based interfaces), Storybook a11y addon.
  2. Colour-blind simulation: use the Chromium DevTools "Emulate vision deficiency" feature to test all four major deficiency types (protanopia, deuteranopia, tritanopia, achromatopsia).
  3. Manual dark/light comparison: render the key screens in both modes side-by-side and verify no information is lost in either mode.
  4. User preference test: verify that prefers-color-scheme: dark is respected on first load before any user preference is saved.

Accessibility Checklist

Apply this checklist before releasing any new interface or after significant palette changes:

Contrast ratios:

  • All body text tokens meet WCAG 2.1 AA (≥ 4.5:1)
  • All large-text tokens meet WCAG 2.1 AA (≥ 3:1)
  • All non-text UI component colours meet WCAG 2.1 AA (≥ 3:1)
  • No critical information is conveyed by colour alone (always pair with shape, pattern, or label)

Colour blindness:

  • Categorical palette verified safe for deuteranopia (most common: red-green)
  • Status palette verified safe for deuteranopia and protanopia
  • Any red-green combination has a secondary distinguishing channel

Dark / light mode:

  • All semantic tokens have explicit dark and light values defined
  • No component references base tokens directly (always via semantic tokens)
  • Mode switch tested without page reload
  • System preference (prefers-color-scheme) is respected on first load

Token hygiene:

  • No hardcoded hex values in component stylesheets
  • All new tokens added to the base → semantic → component hierarchy
  • Token names follow the naming convention (--[category]-[role]-[modifier])
  • Deprecated tokens are listed with a migration path, not silently removed