Design System
Primitives · Semantics · Typography · Components
⌘K
How It Works
From a Figma color change to production code — the full team workflow using GitHub and CI automation.
1
Designer updates Figma
Changes a color, spacing value, or semantic token in Figma Variables. This is the only place design decisions are made.
2
Export variables.json
Re-exports using the Variables2JSON Figma plugin. This file is the raw Figma format — not yet W3C compliant, that happens in the next step.
3
Open a pull request
Commits the updated
variables.json to a branch and opens a PR. This is the review gate — nothing ships until it's approved.4
GitHub Action runs automatically
Triggered by the PR. Runs
npm run build — three steps in sequence:
1
transform.js
Converts Figma format → W3C
$value/$type
2
validate.js
Checks 500+ token references, fails the build if anything is broken
3
sd.config.js
Generates CSS, JS, TypeScript types, Swift, Android XML
5
Review the diff
The action commits the rebuilt
build/ files back to the PR. Reviewers see the exact change across every platform before it ships.build/web/tokens.css - --semantic-surface-bg-brand: #4A5C2A; + --semantic-surface-bg-brand: #3B4A22; build/ios/TokensLight.swift - UIColor(red: 0.290, green: 0.361, blue: 0.165) + UIColor(red: 0.231, green: 0.290, blue: 0.133) build/android/values/colors.xml - <color name="semantic_surface_bg_brand">#4A5C2A</color> + <color name="semantic_surface_bg_brand">#3B4A22</color>
6
Merge
All platform outputs are updated. Developers
git pull and their UI reflects the new token values — no code changes on their side.Primitive Colors
Raw color scales. These are the building blocks — reference semantic tokens in your code, not these directly.
Scale
Numeric scale values used for spacing, sizing, and border radius.
Font Primitives
Base font-family and font-weight tokens.
Semantic Tokens
Context-aware tokens that change value between light and dark mode. Use these in your code.
Content — Text
Content — Icons
Surface — Background
Surface — Border
Primary
Status — Error
Status — Success
Status — Warning
Status — Info
Neutral
Disabled
Selected / Focus / Border
Typography
Type styles using the Geist font family.
Headings
Body
Responsive Type Scale
Font sizes across Desktop, Tablet, and Mobile breakpoints.
For Teams
Style Dictionary generates platform-specific files from the same token source.
Each team copies or imports their file — no manual translation needed.
How the pipeline works
One command generates all platform outputs from your Figma export.
npm run build # 1. transform.js — converts figma-tokens.json → tokens/*.json (Style Dictionary format) # 2. sd.config.js — runs Style Dictionary → build/ (platform-specific files)
Generated files:
build/web/tokens.css build/web/tokens.js build/ios/TokensLight.swift build/ios/TokensDark.swift build/android/values/colors.xml build/android/values-night/colors.xml build/rn/tokens.jsWeb — CSS
/* ─── 1. Import once in your app entry point ─── */
<link rel="stylesheet" href="build/web/tokens.css">
/* tokens.css contains BOTH light (default) and dark mode:
:root { --color-lime-500: #ACFF0F; ... }
[data-theme="dark"] { --semantic-surface-bg-default: #000000; ... }
*/
/* ─── 2. Use semantic tokens in your components ─── */
.button-primary {
background-color : var(--component-button-primary-bg-default);
color : var(--component-button-primary-text-default);
height : var(--component-button-md-height);
border-radius : var(--component-button-border-radius);
padding : 0 var(--component-button-md-padding-x);
}
.button-primary:hover {
background-color: var(--component-button-primary-bg-hover);
}
/* ─── 3. Toggle dark mode by setting data-theme on <html> ─── */
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.removeAttribute('data-theme'); /* back to light */
Web — JavaScript / TypeScript
// ─── Named ES module exports (resolved values) ───────────────
import {
componentButtonPrimaryBgDefault,
componentButtonPrimaryTextDefault,
componentButtonMdHeight,
componentButtonBorderRadius,
semanticContentTextHeading,
colorLime500,
} from './build/web/tokens.js';
// Use in React / vanilla JS
const buttonStyles = {
backgroundColor : componentButtonPrimaryBgDefault, // '#ACFF0F'
color : componentButtonPrimaryTextDefault, // '#000000'
height : componentButtonMdHeight, // '36px'
borderRadius : componentButtonBorderRadius, // '8px'
};
// TypeScript: values are typed as string
// For stricter types, add a d.ts file or use the CSS approach.
iOS (Swift / UIKit)
Copy build/ios/TokensLight.swift and TokensDark.swift
into your Xcode project. Switch between structs based on
traitCollection.userInterfaceStyle,
or create a wrapper that uses dynamic colors.
// ─── TokensLight.swift (auto-generated) ───────────────────────
import UIKit
public struct TokensLight {
public static let colorLime500 = UIColor(red: 0.675, green: 1.000, blue: 0.059, alpha: 1.000)
public static let semanticPrimaryBgSolid = UIColor(red: 0.675, green: 1.000, blue: 0.059, alpha: 1.000)
public static let componentButtonPrimaryBgDefault = UIColor(red: 0.675, green: 1.000, blue: 0.059, alpha: 1.000)
public static let componentButtonBorderRadius: CGFloat = 8
// ... hundreds more
}
// ─── Usage in a UIKit view ─────────────────────────────────────
class PrimaryButton: UIButton {
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
super.traitCollectionDidChange(previous)
applyTokens()
}
func applyTokens() {
let t = traitCollection.userInterfaceStyle == .dark ? TokensDark.self : TokensLight.self
backgroundColor = t.componentButtonPrimaryBgDefault
layer.cornerRadius = t.componentButtonBorderRadius
}
}
// ─── Or use UIColor dynamic provider (iOS 13+) ─────────────────
let brandColor = UIColor { traits in
traits.userInterfaceStyle == .dark
? TokensDark.componentButtonPrimaryBgDefault
: TokensLight.componentButtonPrimaryBgDefault
}
Android (XML resources)
Place build/android/values/colors.xml in
res/values/
and build/android/values-night/colors.xml in res/values-night/.
Android automatically switches between them based on system dark mode — no code needed.
<!-- build/android/values/colors.xml (light — auto-generated) ──────────────────────────── --> <?xml version="1.0" encoding="UTF-8"?> <resources> <color name="color_lime_500">#ACFF0F</color> <color name="semantic_primary_bg_solid">#ACFF0F</color> <color name="component_button_primary_bg_default">#ACFF0F</color> <!-- ... --> </resources> <!-- build/android/values/dimens.xml (auto-generated) ───────────────────────────────── --> <resources> <dimen name="component_button_border_radius">8dp</dimen> <dimen name="component_button_md_height">36dp</dimen> <!-- ... --> </resources> <!-- Usage in a button style (styles.xml) ────────────────────────────────────────────── --> <style name="Widget.App.Button.Primary"> <item name="android:background">@color/component_button_primary_bg_default</item> <item name="android:minHeight">@dimen/component_button_md_height</item> <item name="android:paddingStart">@dimen/component_button_md_padding_x</item> </style> <!-- Android picks values-night/colors.xml automatically in dark mode ✓ -->
React Native
Import build/rn/tokens.js. The file exports a nested object
with fully resolved values. Use with
useColorScheme() to handle light/dark
— build two rn/tokens files (light + dark) for full mode support.
// build/rn/tokens.js (auto-generated, light mode values)
const tokens = {
color: { lime: { "500": "#ACFF0F" }, ... },
semantic: {
primary: { bg: { solid: "#ACFF0F" } },
content: { text: { heading: "#000000" } },
},
component: {
button: {
primary: { bg: { default: "#ACFF0F" }, text: { default: "#000000" } },
"border-radius": 8,
md: { height: 36, "padding-x": 16 },
},
},
};
// ─── Usage in a React Native component ────────────────────────
import { StyleSheet, useColorScheme } from 'react-native';
import lightTokens from './build/rn/tokens';
function PrimaryButton({ label }) {
const scheme = useColorScheme();
const t = scheme === 'dark' ? darkTokens : lightTokens; // swap per mode
const styles = StyleSheet.create({
button: {
backgroundColor : t.component.button.primary.bg.default,
height : t.component.button.md.height,
paddingHorizontal: t.component.button.md['padding-x'],
borderRadius : t.component.button['border-radius'],
},
label: {
color: t.component.button.primary.text.default,
},
});
return <TouchableOpacity style={styles.button}><Text style={styles.label}>{label}</Text></TouchableOpacity>;
}
Token Governance
Architecture, naming conventions, usage rules, and WCAG AAA compliance for the Hopper token system.
4
Collections
136
Semantic vars
140
Component vars
2
Theme modes
Architecture
Three-tier system. Each tier has one job. Never skip a tier — always resolve through primitives → semantics → component tokens.
Token chain:
primitives
→
semantics
→
component tokens
Primitives
Raw palette, spacing, and type. No semantic meaning. Never applied directly to components.
Semantics
Role-based design language. Where all color decisions live. Switching mode controls the entire theme.
Component Tokens
Per-component structure pointing into semantics. Single mode — theming flows automatically.
Token Naming
Every semantic token follows a consistent four-segment path. Learning this pattern means you can predict any token name without looking it up.
group
/
role
/
property
/
state
| Segment | What it is | Values |
|---|---|---|
group | The domain or role category | surface · content · primary · neutral · status · disabled · selected · focus |
role | Status variant (status/* only) | error · success · warning · info |
property | What is being styled | bg · text · icon · border |
state | Visual variant or interaction state | solid · subtle · solid-hover · subtle-hover |
WCAG AAA Compliance
All semantic token pairings have been contrast-tested. Use this as the contract for what is and isn't acceptable in the Hopper system.
Passes WCAG AAA — use freely
content/text/heading+bodyon allsurface/bg/*— 13:1 to 21:1primary/bg/solid+primary/text/solid— 17:1 both modesneutral/bg/solid+neutral/text/solid— 9.9:1 both modes- All
status/*/text/solidonstatus/*/bg/solid(except warning) — 8:1+ selected/textonselected/bg— 13.4:1focus/ringon canvas — 13.6:1 light · 16.6:1 darkcontent/text/linkonsurface/bg/default— 13.6:1 both modes
AA only — fine for UI chrome, avoid for body text
status/warning/*solid or subtle pairings — 5.66:1 (yellow physical limit)status/error/text/solidonstatus/error/bg/solidin dark mode — 5.2:1content/text/captiononsurface/bg/*— 4.5:1 to 5.8:1content/text/placeholderonsurface/bg/default— 5.06:1 light
Never use — fails WCAG
content/text/placeholderonsurface/bg/raisedor/sunken— drops below 3:1- Any
primary/bg/subtleas a standalone selection signal on white — 1.25:1 (invisible) - Any primitive color directly on any element — unvalidated
- Mixing token suffixes: solid text on subtle bg or vice versa
⚠ Warning tokens (yellow) max at AA — this is a physical limit of the yellow hue. For body text in warning contexts, place content on
surface/bg/default instead.
7 Rules
These rules protect the system's integrity, WCAG compliance, and theming correctness.
1Never use primitives directly
Always go through semantics. Primitives have no WCAG validation and bypass the theming system.
2Never mix token suffix pairs
solid text only on solid bg. subtle text only on subtle bg. Never cross-pair suffixes.
3No hover states on disabled
Disabled elements cannot be hovered. Those tokens do not exist by design.
4Lime tints need a border
primary/bg/subtle is 1.25:1 on white — invisible alone. Always pair with selected/border as the real signal.5Focus ring: 2px minimum
Use
border-width/md. Always include focus/ring-offset to create the gap between element and ring.6Dark mode lives in semantics only
Component tokens stay single-mode. Switch the semantics collection mode to theme the entire UI.
7Re-check WCAG after palette changes
Run the contrast audit script any time a primitive value is updated.
Known Issues
Medium
surface/border/default → foundation/white
A white border on a white surface is invisible. Review intended use before changing — may need
gray/200.
Low
content/text/link-hover = same as link in light
Hover state uses underline, not color change. Intentional — document for engineers implementing in code.
Low
font-weight/Semi Bold has a space
Inconsistent with Regular and Medium. Rename to
SemiBold if tooling is case-sensitive.
Accepted
Warning tokens max at AA
Yellow hue physical limit. Avoid warning text on yellow bg for body copy. See WCAG section.
Component Tokens
Design decisions scoped to individual components. These alias semantic tokens.
Button
Input
Badge
Alert
Checkbox
Toggle
How This Was Built
A full design system pipeline — from blank Figma canvas to W3C-compliant tokens generating platform code —
built incrementally with AI. This is the real process behind it.