02 — Design Tokens Extraction from Figma
Dev Guide — Savoy Signature Hotels
PRD refs:05_Design_System_and_Theming.md,A03_Design_Tokens.md
1. Purpose
Section titled “1. Purpose”This guide defines how design tokens are extracted from the Figma 01_Savoy_library file and translated into the CSS variable system used by the Savoy platform. Design tokens are the foundation of the multi-site theming system — every visual property flows from Figma variables through a scripted extraction pipeline into CSS custom properties.
2. Token Architecture
Section titled “2. Token Architecture”2.1 Data Flow
Section titled “2.1 Data Flow”Figma 01_Savoy_library (file key VFdCObxYyZjSRFdK5pU1df) └─ Native variables export (W3C <Mode>.tokens.json per collection) | v scripts/figma-tokens/adapt-figma-export.mjs (re-links aliases by targetVariableName, per-mode brand disambiguation, value-integrity check) | v scripts/figma-tokens/figma-variables.json ← committed dump (1 197 variables) | v scripts/figma-tokens/generate-themes.mjs + config.mjs (curation: skips, no rename rules — mechanical name normalisation only) | v packages/themes/src/*.css ← CANONICAL CSS (hand-maintained after first run) | v scripts/figma-tokens/tokens-manifest.json ← feeds Storybook Foundations pagesLifecycle rule: After the initial PR that introduced this pipeline, the CSS files in packages/themes/src/ are the canonical source of truth and are hand-maintained. The dump + generator scripts remain as the extraction record and a re-run tool only. If the generator is ever re-run, run Prettier over the output before diffing — pnpm lint-staged reformats quotes etc. on commit.
2.2 Figma Variable Collections
Section titled “2.2 Figma Variable Collections”The Figma library (01_Savoy_library, file key VFdCObxYyZjSRFdK5pU1df) exposes these collections:
| Collection | Variables | Modes | Purpose |
|---|---|---|---|
Alias | 645 | Savoy Palace, Saccharum, Calheta Beach, Next, Signature, The Reserve, Royal Savoy | Per-hotel resolved tokens — THE layer new code consumes |
Responsive | 190 | Desktop, Mobile | Text sizes, paddings, layout, radius-containers (responsive overrides) |
Brand - Global | 99 | (single) | Colour palettes + 41-step scale/* spacing primitives |
Brand - Savoy Palace … Brand - Hotel Next | 31–43 each | (single per brand) | Brand colours + font family/weight strings |
color | 12 | — | ORPHAN — referenced by nothing; skip; flagged for deletion in Figma |
Note: There is no “Gardens” mode in the
Aliascollection —gardens.cssis a stub that inherits Signature values. It will be replaced when a Gardens design is delivered.
2.3 CSS Layer Stack
Section titled “2.3 CSS Layer Stack”Files are imported in this order in packages/themes/src/index.css:
| File | Layer | Selector | What it contains |
|---|---|---|---|
_reset.css, _fonts.css | Reset + @font-face | (global) | Browser normalisation; @font-face declarations — unchanged |
_primitives.css | Primitives | :root | Brand - Global: colour palettes + --scale-* spacing steps |
_responsive.css | Responsive | :root (mobile) + @media (min-width: 1024px) (desktop) | Text sizes, paddings, layout widths, radius-containers |
_base.css | Code-only | :root | Tokens with no Figma counterpart: --z-*, --transition-*, --container-*, --header-height |
<theme>.css × 8 | Alias / theme | [data-theme="<site-key>"] | Brand primitives + all 645 alias tokens resolved for that hotel’s mode — new code must consume tokens from this layer |
_compat.css | Compat shim | :root | Legacy pre-Figma names (--color-primary, --space-*, --text-*, module tokens) aliased to primitives or kept literal — FROZEN; existing modules only; never consume in new code |
Theme applied via <html data-theme="{siteKey}"> — zero JS overhead.
3. Extraction Process (Current Procedure)
Section titled “3. Extraction Process (Current Procedure)”The extraction was performed as a one-shot run on 2026-06-11. The steps below document what was done and how to repeat it if Figma variables change significantly.
3.1 Export from Figma
Section titled “3.1 Export from Figma”- Open
01_Savoy_libraryin Figma desktop (file keyVFdCObxYyZjSRFdK5pU1df) - Use the native Figma variables export — produces zips of W3C
<Mode>.tokens.jsonper collection - Place the unzipped JSONs where
adapt-figma-export.mjsexpects them (see the script’s inline comments for paths)
3.2 Adapt the Export
Section titled “3.2 Adapt the Export”node scripts/figma-tokens/adapt-figma-export.mjsThe adapter:
- Re-links aliases by
targetVariableName(the Figma-internal reference) - Applies per-mode brand disambiguation (each hotel mode resolves to the correct
Brand - *collection) - Runs a value-integrity check and warns on broken references
- Writes
scripts/figma-tokens/figma-variables.json(1 197 variables, committed)
3.3 Generate CSS
Section titled “3.3 Generate CSS”node scripts/figma-tokens/generate-themes.mjsUses scripts/figma-tokens/config.mjs for curation (collections to skip, no rename rules). Applies mechanical name normalisation only (see §3.4).
Unit-tested utilities live in scripts/figma-tokens/lib/. Run tests with:
node --test scripts/figma-tokens/lib/*.test.mjs3.4 Token Naming Normalisation
Section titled “3.4 Token Naming Normalisation”Names are derived mechanically from the Figma variable path — zero manual rename rules:
| Rule | Detail |
|---|---|
| Lowercase | All path segments converted to lowercase |
colour/colours → color | British spelling normalised |
Spaces and underscores → - | All other non-alphanumeric chars stripped |
Segments joined with - | Full path becomes the property name |
Examples:
| Figma path | CSS custom property |
|---|---|
colours/palace gold/500 | --color-palace-gold-500 |
colour/surface/background/A | --color-surface-background-a |
scale/600 (24) | --scale-600-24 |
text sizes/display xl/font size | --text-sizes-display-xl-font-size |
Rule: The name shown in Figma Dev Mode IS the CSS custom property (after normalisation). Do not invent or shorten names.
3.5 Compat Shim Generation
Section titled “3.5 Compat Shim Generation”scripts/figma-tokens/suggest-compat.mjs generated the initial _compat.css draft. The file is now hand-maintained and FROZEN — do not regenerate it without a deliberate Phase 2 decision.
3.6 Legacy Extraction Tables (Removed)
Section titled “3.6 Legacy Extraction Tables (Removed)”The former §3.x “hand-inspection” tables (color roles, Figma inspector steps) described a manual process that no longer applies. They have been removed. All token values now originate from figma-variables.json via the generator.
4. Token Reference for New Code
Section titled “4. Token Reference for New Code”4.1 Which Tokens to Use
Section titled “4.1 Which Tokens to Use”| Do | Do not |
|---|---|
Consume Alias tokens from <theme>.css (e.g. --color-surface-background-a) | Consume _compat.css names (--color-primary, --space-*) in new code |
Consume Brand - Global primitives from _primitives.css where semantically appropriate (e.g. --scale-*) | Reference hardcoded colour values |
Consume Responsive tokens from _responsive.css for text sizes, paddings, radii | Invent one-off values not in Figma |
Use code-only tokens from _base.css for z-index, transitions, container layout | Mix compat and alias names in the same new file |
4.2 Token Prefix Reference (New Architecture)
Section titled “4.2 Token Prefix Reference (New Architecture)”| Prefix | Layer | Purpose | Example |
|---|---|---|---|
--color-* | Primitives + Alias | Colour palette + semantic surface/text/border aliases | --color-surface-background-a, --color-palace-gold-500 |
--scale-* | Primitives | 41-step spacing scale (4px base) from Brand - Global | --scale-600-24 |
--text-sizes-* | Responsive | Responsive type metrics per role (font size / line height / letter spacing), desktop + mobile modes | --text-sizes-display-xl-font-size, --text-sizes-body-m-font-size |
--paddings-* | Responsive | Responsive padding values | --paddings-lateral-small |
--layout-* | Responsive | Responsive column/layout widths | --layout-2-columns |
--radius-containers-* | Responsive | Container border-radius (responsive) | --radius-containers-palace-op2-top-left |
--fonts-* | Alias | Font family / weight strings per hotel | --fonts-h1-font, --fonts-h1-weight |
--appearance-* | Alias | Per-hotel social network show/hide flags (1px = show, 0 = hide) — controls which social icons render per site | --appearance-socialmedia-facebook |
--z-* | _base.css (code-only) | Z-index scale | --z-header, --z-modal, --z-toast |
--transition-* | _base.css (code-only) | Duration scale | --transition-fast, --transition-normal, --transition-slow |
--container-* | _base.css (code-only) | Layout container sizing/padding | --container-max, --container-padding |
--header-height | _base.css (code-only) | Fixed header height for offset calculations | --header-height |
Legacy compat names (--color-primary, --font-heading, --font-body, --space-*, --text-xs … --text-6xl, --radius-*, --shadow-*, --leading-*) survive in _compat.css for existing modules only. They carry TODO(phase2) comments and will be pruned in the Phase 2 module audit (P894-T769). Never use them in new code.
4.3 Figma-to-CSS Workflow for New Features
Section titled “4.3 Figma-to-CSS Workflow for New Features”- Inspect the element in Figma Dev Mode — the variable name shown IS the CSS custom property (after normalisation)
- Confirm the property exists in the generated CSS:
grep -r '<property>' packages/themes/src/ - Use it via
var(--property-name)— no hardcoding, no compat names
5. Theme File Structure
Section titled “5. Theme File Structure”packages/themes/src/ _reset.css ← Browser reset _fonts.css ← @font-face declarations _primitives.css ← :root — colour palettes + --scale-* spacing _responsive.css ← :root mobile + @media (min-width:1024px) desktop overrides _base.css ← Code-only tokens: --z-*, --transition-*, --container-*, --header-height savoy-signature.css ← [data-theme="savoy-signature"] savoy-palace.css ← [data-theme="savoy-palace"] royal-savoy.css ← [data-theme="royal-savoy"] saccharum.css ← [data-theme="saccharum"] the-reserve.css ← [data-theme="the-reserve"] calheta-beach.css ← [data-theme="calheta-beach"] gardens.css ← [data-theme="gardens"] — STUB (inherits Signature; replace when design delivers) hotel-next.css ← [data-theme="hotel-next"] _compat.css ← FROZEN legacy compat shim — existing modules only index.css ← Imports in the order above tokens-manifest.json ← Generated; feeds Storybook Foundations (Colors/Typography/Spacing)6. Hard Rules
Section titled “6. Hard Rules”- New code consumes Alias tokens and primitives only —
_compat.cssis a frozen pruning backlog targeted by the Phase 2 module audit (P894-T769). gardens.cssis a stub inheriting Signature values — no Gardens design exists yet. Replace when design delivers.- Fonts are Figma variables —
--fonts-*alias tokens carry family/weight strings per hotel. Legacy--font-heading/--font-bodysurvive in compat until Phase 2. tokens-manifest.json(generated alongside CSS) feeds the StorybookFoundationspages (Colors, Typography, Spacing). Do not edit it by hand.- Old
--space-*and--text-*scales survive in compat only — the Figma spacing primitives are--scale-*and responsive sizes are--text-sizes-*.
7. Validation Checklist
Section titled “7. Validation Checklist”After any theme update:
- Token names match Figma Dev Mode (normalisation applied correctly)
- All colour tokens have sufficient contrast (4.5:1 for text, 3:1 for large text)
- No hardcoded values in theme CSS — only
var(--token)references - Font families load correctly (check Network tab — no 404s)
- No layout shift from font loading (
font-display: swap) - Theme file renders correctly in Storybook theme switcher
- All existing modules render correctly with updated theme
-
tokens-manifest.jsonupdated if primitive/responsive tokens changed - Unit tests pass:
node --test scripts/figma-tokens/lib/*.test.mjs