-
Notifications
You must be signed in to change notification settings - Fork 63
docs: add comprehensive guide to color scales and contrast patterns #1361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
ac62836
6483118
d7ff2b1
c06c377
c56a400
9a65447
dcb8dd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| npx tsx scripts/checkRemovedTokens.ts --base HEAD | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,274 @@ | ||||||||||
| # Color Scales in Primer Design Tokens | ||||||||||
|
|
||||||||||
| This document explains how color scales work in primer/primitives, how they're structured, and the constraints that guide them. | ||||||||||
|
|
||||||||||
| ## Overview | ||||||||||
|
|
||||||||||
| Primer's color system is built on **base color scales** — semantic palettes (neutral, blue, green, red, etc.) that serve as the foundation for **functional tokens** (fgColor, bgColor, borderColor, etc.). | ||||||||||
|
|
||||||||||
| - **Base scales:** Raw color values organized by hue and lightness step | ||||||||||
| - **Functional tokens:** Semantic aliases that layer contrast requirements, accessibility rules, and theme-aware logic on top of base scales | ||||||||||
| - **Themes:** Light, dark, dark-dimmed, high-contrast variants that reuse the same functional token names with different values | ||||||||||
|
|
||||||||||
| ## Base Color Scales | ||||||||||
|
|
||||||||||
| ### Scale Structure | ||||||||||
|
|
||||||||||
| Each base scale has **13 steps** (0–13): | ||||||||||
|
||||||||||
|
|
||||||||||
| | Step | Purpose | Light Theme (neutral example) | | ||||||||||
| |------|---------|-------------------------------| | ||||||||||
| | 0 | **Brightest/Lightest** | `#ffffff` (white) | | ||||||||||
| | 1–7 | **Light backgrounds** | `#f6f8fa` → `#c8d1da` (gradual darkening) | | ||||||||||
| | 8–11 | **Text & borders** | `#818b98` → `#393f46` (medium to dark gray) | | ||||||||||
| | 12–13 | **Darkest/Highest contrast** | `#25292e` → `#1f2328` (black) | | ||||||||||
|
|
||||||||||
| **Why 13 steps?** They provide: | ||||||||||
| - Enough granularity for fine contrast control (especially in mid-tones, steps 5–8) | ||||||||||
| - Semantic meaning tied to use cases (light backgrounds, text, borders) | ||||||||||
| - Predictable progression for interpolation and theme adaptation | ||||||||||
|
|
||||||||||
| ### Current Scales | ||||||||||
|
|
||||||||||
| Primer defines base scales for: | ||||||||||
| - **Neutral** (gray, used for defaults, text, borders) | ||||||||||
| - **Blue** (primary accent color) | ||||||||||
| - **Green** (success) | ||||||||||
| - **Red** (danger) | ||||||||||
| - **Yellow** (warning) | ||||||||||
| - **Purple** (emphasis) | ||||||||||
| - Plus specialized scales: orange, pink, coral, lime, cyan, indigo | ||||||||||
|
|
||||||||||
| ### Scale Files | ||||||||||
|
|
||||||||||
| All base scales are defined in JSON5 files: | ||||||||||
|
|
||||||||||
| ``` | ||||||||||
| src/tokens/base/color/ | ||||||||||
| ├── light/ | ||||||||||
| │ ├── light.json5 # Base light theme (default 0-13 steps) | ||||||||||
| │ ├── light.high-contrast.json5 | ||||||||||
| │ ├── display-light.json5 # Display color scales (accent colors) | ||||||||||
| ├── dark/ | ||||||||||
| │ ├── dark.json5 | ||||||||||
| │ ├── dark.dimmed.json5 | ||||||||||
| │ ├── dark.high-contrast.json5 | ||||||||||
| │ └── display-dark.json5 | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ## Functional Tokens Layer | ||||||||||
|
|
||||||||||
| ### Why Functional Tokens Exist | ||||||||||
|
|
||||||||||
| **Problem:** If consumers directly used base scales, they'd have to: | ||||||||||
| - Know which step is safe for text on which background | ||||||||||
| - Remember which step means "hover" vs. "active" | ||||||||||
| - Rewrite logic for each new theme | ||||||||||
|
|
||||||||||
| **Solution:** Functional tokens abstract this complexity: | ||||||||||
|
|
||||||||||
| ```json5 | ||||||||||
| // Bad: consumer has to think about steps | ||||||||||
| color: $base.color.neutral.5 // Is this readable on neutral.0? Need to check contrast | ||||||||||
|
|
||||||||||
| // Good: semantic name, works across all themes | ||||||||||
| color: $base.color.fgColor.default // Guaranteed to meet 4.5:1 contrast on default bg | ||||||||||
|
lukasoppermann marked this conversation as resolved.
Outdated
|
||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ### Examples of Functional Tokens | ||||||||||
|
|
||||||||||
| | Token | Purpose | Light Value | Dark Value | | ||||||||||
| |-------|---------|-------------|-----------| | ||||||||||
| | `fgColor.default` | Main text | `neutral.12` | `neutral.1` | | ||||||||||
| | `fgColor.muted` | Secondary text (lower contrast) | `neutral.8` | `neutral.4` | | ||||||||||
| | `bgColor.default` | Main background | `neutral.0` | `neutral.13` | | ||||||||||
| | `bgColor.emphasis` | Highlighted background | `blue.1` | `blue.9` | | ||||||||||
| | `borderColor.default` | Standard border | `neutral.6` | `neutral.7` | | ||||||||||
|
|
||||||||||
| ### Functional Token Structure | ||||||||||
|
|
||||||||||
| Functional tokens live in: | ||||||||||
|
|
||||||||||
| ``` | ||||||||||
| src/tokens/functional/ | ||||||||||
| ├── color/ | ||||||||||
| │ ├── fgColor.json5 | ||||||||||
| │ ├── bgColor.json5 | ||||||||||
| │ ├── borderColor.json5 | ||||||||||
| │ ├── control.json5 # Buttons, inputs, selects | ||||||||||
| │ ├── button.json5 | ||||||||||
| │ ├── avatar.json5 | ||||||||||
| │ ├── counter.json5 | ||||||||||
| │ └── [100+ more specialized tokens] | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Each functional token file references base scales: | ||||||||||
|
|
||||||||||
| ```json5 | ||||||||||
| { | ||||||||||
| fgColor: { | ||||||||||
| default: { | ||||||||||
| $value: '{base.color.neutral.12}', // ← References step 12 of neutral scale | ||||||||||
| $type: 'color' | ||||||||||
| }, | ||||||||||
| muted: { | ||||||||||
| $value: '{base.color.neutral.8}', // ← Step 8 for lower contrast | ||||||||||
| $type: 'color' | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| **Theme-aware behavior:** | ||||||||||
| The same `fgColor.default` token can resolve to different values in light vs. dark themes by updating the reference: | ||||||||||
|
|
||||||||||
| ```json5 | ||||||||||
| // light.json5 | ||||||||||
| fgColor.default: '{base.color.neutral.12}' // Dark text on light bg | ||||||||||
|
|
||||||||||
| // dark.json5 | ||||||||||
| fgColor.default: '{base.color.neutral.1}' // Light text on dark bg | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ## Contrast Requirements | ||||||||||
|
|
||||||||||
| Primer enforces [WCAG 2.1 AA compliance](https://www.w3.org/TR/WCAG21/) via documented contrast rules: | ||||||||||
|
|
||||||||||
| ### Text Contrast | ||||||||||
|
|
||||||||||
| | Pair | Ratio | Rule | | ||||||||||
| |------|-------|------| | ||||||||||
| | Text vs. background | **4.5:1** | Required for all text | | ||||||||||
| | Links vs. surrounding text | **3:1** | In addition to 4.5:1 against bg | | ||||||||||
| | Placeholder text vs. bg | **4.5:1** | Or use visible label | | ||||||||||
| | Disabled text | None | No requirement | | ||||||||||
|
|
||||||||||
| ### Non-Text Contrast | ||||||||||
|
|
||||||||||
| | Pair | Ratio | Rule | | ||||||||||
| |------|-------|------| | ||||||||||
| | Focus border vs. adjacent colors | **3:1** | Outline around interactive controls | | ||||||||||
| | Empty control border vs. bg | **3:1** | Unfilled buttons, empty inputs | | ||||||||||
| | States shown together | **3:1** | Selected vs. unselected in segmented controls | | ||||||||||
| | Decorative elements | None | No requirement | | ||||||||||
|
|
||||||||||
| ### Contrast Checking | ||||||||||
|
|
||||||||||
| Primer runs automated contrast checks via `scripts/colorContrast.ts`: | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| npm run build:tokens # Generates all token files | ||||||||||
| npm run check:contrast # Runs contrast test | ||||||||||
|
Comment on lines
+160
to
+161
|
||||||||||
| npm run build:tokens # Generates all token files | |
| npm run check:contrast # Runs contrast test | |
| npm run build:tokens # Generates all token files | |
| npm run validate:contrast # Runs contrast test |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.