How It Works
From a Figma color change to production code — the full team workflow using GitHub and CI automation.
🎨
Designer
Updates a color or token in Figma variables
📤
Export
Re-exports variables.json using Variables2JSON plugin
🌿
Branch + PR
Commits the new file to a branch, opens a pull request
⚙️
GitHub Action
Validates all references, runs npm run build, commits updated build/ back to the PR. Fails if any token is broken.
👀
Review
Team sees exact diff — CSS variables, TypeScript types, Swift constants, Android colors — across every platform
Merge
All platforms updated. Developers pull and get the new tokens immediately
The GitHub Action

Lives in .github/workflows/build-tokens.yml. Triggers only when variables.json changes — not on every push.

on:
  pull_request:
    paths:
      - variables.json    # only runs when the Figma export changes

jobs:
  build-tokens:
    runs-on: ubuntu-latest
    steps:
      - checkout the PR branch
      - npm ci
      - npm run build
          # 1. transform.js  → converts variables.json to W3C tokens/
          # 2. validate.js   → checks all 500+ references, fails if broken
          # 3. sd.config.js  → generates CSS, JS, TS types, Swift, Android
      - git commit tokens/ build/ back to the PR branch
What the reviewer sees

When the action runs, it pushes the rebuilt files back to the PR. The diff shows exactly what changed — across every platform — before anything ships.

# build/web/tokens.css
- --semantic-surface-bg-brand: #4A5C2A;
+ --semantic-surface-bg-brand: #3B4A22;

# build/web/tokens.d.ts  ← TypeScript types updated too
- export declare const semanticSurfaceBgBrand: string;   (unchanged, but value behind it changed)

# build/ios/TokensLight.swift
- public static let semanticSurfaceBgBrand = UIColor(red: 0.290, green: 0.361, blue: 0.165, alpha: 1.000)
+ public static let semanticSurfaceBgBrand = UIColor(red: 0.231, green: 0.290, blue: 0.133, alpha: 1.000)

# build/android/values/colors.xml
- <color name="semantic_surface_bg_brand">#4A5C2A</color>
+ <color name="semantic_surface_bg_brand">#3B4A22</color>
What the developer does

Nothing changes in how developers write code. They already reference tokens by name. When the value behind a token changes, their UI updates automatically.

/* Developer wrote this once — never needs to change it */
.hero {
  background: var(--semantic-surface-bg-brand);
}

/* After the PR merges and they pull:
   git pull → the new tokens.css is there → UI reflects the new color */
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.js
Web — 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
90 vars · 1 mode
Raw palette, spacing, and type. No semantic meaning. Never applied directly to components.
Semantics
136 vars · Light + Dark
Role-based design language. Where all color decisions live. Switching mode controls the entire theme.
Component Tokens
140 vars · 1 mode
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
SegmentWhat it isValues
groupThe domain or role categorysurface · content · primary · neutral · status · disabled · selected · focus
roleStatus variant (status/* only)error · success · warning · info
propertyWhat is being styledbg · text · icon · border
stateVisual variant or interaction statesolid · 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 + body on all surface/bg/* — 13:1 to 21:1
  • primary/bg/solid + primary/text/solid — 17:1 both modes
  • neutral/bg/solid + neutral/text/solid — 9.9:1 both modes
  • All status/*/text/solid on status/*/bg/solid (except warning) — 8:1+
  • selected/text on selected/bg — 13.4:1
  • focus/ring on canvas — 13.6:1 light · 16.6:1 dark
  • content/text/link on surface/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/solid on status/error/bg/solid in dark mode — 5.2:1
  • content/text/caption on surface/bg/* — 4.5:1 to 5.8:1
  • content/text/placeholder on surface/bg/default — 5.06:1 light
Never use — fails WCAG
  • content/text/placeholder on surface/bg/raised or /sunken — drops below 3:1
  • Any primary/bg/subtle as 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.