Skip to content
Full Theme Guide
AutoXXS (320px)XS (375px)SM (640px)MD (768px)LG (1024px)XL (1280px)XXL (1536px)
SketchMaterialiOSTamagui
DataInjectionKeyPatternsServiceTransactionProcessResearchProductQualityPerformanceSpecDomainFunctionTechnologyArchitectureConfigMiddlewareDataDatabaseDrizzleMigrationModelop-sqliteSchemaSQLState ManagementDraftKeystoneMergePatchPatchesPersistenceReactiveRedoStoreUndoTestingDeviceFactoryIsolationTypeScriptZodTopicsCommunicationBidsNVCDesignDesign ImplicationsEducationPedagogyFoundationsPsychologyAttachmentFloodingRelatingAuthentic RelatingUIEditorReact Native

Full Theme Guide

Web Awesome Theme Design Guide

A step-by-step process for translating visual references (screenshots, brand guidelines, textual descriptions) into a Web Awesome theme CSS file. The guide covers initial setup and iterative refinement across every design dimension the token system exposes.


How a Theme Works

A Web Awesome theme is a CSS file that overrides design tokens within a @layer wa-theme block. The selector pattern is:

@layer wa-theme {
.wa-theme-{name},
.wa-theme-{name}.wa-light,
.wa-theme-{name} .wa-light,
.wa-theme-{name}.wa-dark .wa-invert,
.wa-theme-{name} .wa-dark .wa-invert,
.wa-light .wa-theme-{name},
.wa-dark .wa-theme-{name}.wa-invert,
.wa-dark .wa-theme-{name} .wa-invert {
/* light-mode token overrides */
}
.wa-theme-{name}.wa-dark,
.wa-theme-{name} .wa-dark,
.wa-theme-{name}.wa-invert,
.wa-theme-{name} .wa-invert,
.wa-dark .wa-theme-{name},
.wa-light .wa-theme-{name}.wa-invert,
.wa-light .wa-theme-{name} .wa-invert {
/* dark-mode token overrides */
}
/* tokens shared between light and dark */
.wa-theme-{name},
.wa-theme-{name}.wa-light,
.wa-theme-{name} .wa-light,
.wa-theme-{name}.wa-dark,
.wa-theme-{name} .wa-dark {
/* typography, spacing, borders, shadows, transitions */
}
}

Apply the theme by adding its class to <html>:

<html class="wa-theme-{name} wa-palette-{palette} wa-brand-{hue}">

The palette class (wa-palette-bright, etc.) loads the color scale. The brand hue (wa-brand-indigo, etc.) maps one of the 10 palette hues onto the --wa-color-brand-* semantic scale.


theme.toml — The Theme Manifest

theme.toml is the Anchor of Truth for StarVision. Explicitly defined values in this file override any visual inference. You only need to declare what is known — StarVision fills the rest from screenshot analysis.

Schema

[theme]
id = "my-theme" # URL-safe slug
label = "My Theme" # Human-readable name
[brand]
# Light mode values — map to --wf-color-* wireframe variables
primary = "#5E6AD2" # Main accent (buttons, active states)
primary_text = "#FFFFFF" # Text on primary bg (must contrast primary)
bg = "#FFFFFF" # Page background
bg_light = "#F7FAFC" # Card / input field background
bg_elevated = "#FFFFFF" # Hover state / active segment background
text = "#1A202C" # Primary text
text_muted = "#718096" # Secondary / hint text
border = "#E2E8F0" # Container borders
border_dark = "#CBD5E0" # Interactive element borders
[brand.dark]
# Dark mode overrides — only specify what differs
primary = "#818CF8"
bg = "#111111"
text = "#EBEBEB"
[semantic]
success = "#38A169"
warning = "#DD6B20"
danger = "#E53E3E"
[geometry]
radius_card = "12px" # --wf-radius-card
radius_button = "8px" # --wf-radius-button
border_width = "1px" # --wf-border-width
spacing_gap = "1rem" # --wf-spacing-gap

Variable Mapping

theme.toml keys map to StarSpec wireframe CSS variables (--wf-*), not directly to WA design tokens. The generated CSS file uses these --wf-* variables as an intermediate layer:

theme.toml keyCSS variableRole in WA theme
brand.primary--wf-color-primaryDrives --wa-color-brand-fill-loud
brand.primary_text--wf-color-primary-textDrives --wa-color-brand-on-loud
brand.bg--wf-color-bgMaps to --wa-color-surface-default
brand.bg_light--wf-color-bg-lightMaps to --wa-color-surface-lowered
brand.bg_elevated--wf-color-bg-elevatedMaps to --wa-color-surface-raised
brand.text--wf-color-textMaps to --wa-color-text-normal
brand.text_muted--wf-color-text-mutedMaps to --wa-color-text-quiet
brand.border--wf-color-borderMaps to --wa-color-surface-border
brand.border_dark--wf-color-border-darkMaps to panel/form control borders
semantic.success--wf-color-successMaps to --wa-color-success-fill-loud
semantic.warning--wf-color-warningMaps to --wa-color-warning-fill-loud
semantic.danger--wf-color-dangerMaps to --wa-color-danger-fill-loud
geometry.radius_card--wf-radius-cardMaps to --wa-panel-border-radius
geometry.radius_button--wf-radius-buttonMaps to --wa-form-control-border-radius
geometry.border_width--wf-border-widthMaps to --wa-border-width-m
geometry.spacing_gap--wf-spacing-gapInforms --wa-space-scale baseline

Rules

  • Partial manifests are valid. Only include keys you know. Omitted keys are inferred from screenshots.
  • Declared values are binding. StarVision must use them exactly — no visual override.
  • Both modes required in the output CSS. Even if [brand.dark] is omitted, the generated file must contain a dark block (inferred).
  • No hardcoded colors in the CSS body. All hex values live in --wf-* variable definitions; the rest of the CSS references those variables.

Process Overview

Work through these phases in order for initial setup. For iteration, jump directly to the relevant section.

  1. Read theme.toml — collect explicit anchor values; mark them as fixed
  2. Analyse screenshots — extract design intent from visual references
  3. Color — palette, brand, surfaces, text, semantic colors
  4. Typography — fonts, size scale, weights, line height
  5. Space — density/rhythm
  6. Borders — corner style, stroke weight
  7. Shadows — elevation character
  8. Focus — accessibility ring
  9. Transitions — motion speed and feel
  10. Component groups — form controls, panels, tooltips
  11. Verify — light/dark parity, accessibility contrast, visual smoke test

Phase 0 — Read theme.toml

Before examining screenshots, parse theme.toml if provided:

  1. Extract every declared key and note its CSS variable and value.
  2. Mark each as fixed — do not infer or override these from screenshots.
  3. Note which keys are absent — these must be inferred in Phase 1.
  4. If [brand.dark] is absent, dark-mode values for those keys must be inferred.

Phase 1 — Analyse Screenshots

Work through the screenshots systematically. For each dimension, extract an observation, then map it to a token or theme.toml key. Skip any dimension already fixed by theme.toml.

Step 1 — Color Extraction

Dominant accent color:

  • Identify the most prominent interactive color (primary button fill, active nav item, link color).
  • Sample its hue. Match to the nearest WA palette hue: red / orange / yellow / green / cyan / blue / indigo / purple / pink / gray.
  • If not fixed by brand.primary: record the hex value → --wf-color-primary.

Background hierarchy:

  • Look for at least two background levels. The lightest is the page canvas; a slightly darker shade appears in cards, input fields, or side panels.
  • Page canvas → --wf-color-bg. Card/input background → --wf-color-bg-light.
  • If any area is noticeably lighter than the canvas (e.g., an active element or hover state), note it as --wf-color-bg-elevated.

Text contrast:

  • Identify primary text color (headings, labels) and secondary text color (hints, placeholders, captions).
  • Primary → --wf-color-text. Secondary → --wf-color-text-muted.
  • Estimate luminance difference: on a white canvas, #1A202C ≈ 15:1, #718096 ≈ 4.8:1.

Border colors:

  • Find the most common divider / container outline.
  • Subtle border (card outline) → --wf-color-border. Stronger border (focused input, table header) → --wf-color-border-dark.

Semantic colors:

  • Look for success banners, warning callouts, error states.
  • Record their approximate hex. If recognizably green/yellow/red, the WA defaults may suffice — only override via [semantic] if the palette is clearly non-standard.

Dark mode:

  • If dark-mode screenshots are present, repeat the above for dark surfaces.
  • Compare: background usually flips from near-white to near-black; primary color often shifts to a lighter tint for contrast.

Step 2 — Geometry

Corner radius:

  • Crop a card or button. Measure visual roundness: near-zero = sharp, 4–8px = professional/moderate, 12–16px = friendly, >20px = heavily rounded.
  • Button/input corners → geometry.radius_button. Card/modal/callout corners → geometry.radius_card.
Visual impressionApproximate values
Sharp / flatradius_button: "2px", radius_card: "4px"
Professionalradius_button: "6px", radius_card: "8px"
Default friendlyradius_button: "8px", radius_card: "12px"
Rounded / modernradius_button: "12px", radius_card: "16px"
Pill buttonsradius_button: "9999px", radius_card: "16px"

Border width:

  • On inputs and cards, is the outline hairline (1px), medium (1.5–2px), or thick (3px+)?
  • Map to geometry.border_width.

Spacing density:

  • Compare button padding and list-item height to a typical 8px grid.
  • Tight (≤8px padding) → --wa-space-scale: 0.875. Default → 1. Airy (≥16px padding) → 1.125.

Step 3 — Typography

Font personality:

  • Serif → editorial/authoritative. Geometric sans → modern/minimal. Humanist sans → friendly. Monospace body → developer tool.
  • If a specific typeface is identifiable (e.g., Inter, SF Pro, Georgia), record its name for --wa-font-family-body.

Type weight:

  • Are button labels and nav items bold or regular weight? Bold → --wa-font-weight-action: 600+. Medium → 500. Regular → 400.
  • Are headings significantly heavier than body text, or the same weight?

Type scale:

  • Compare heading size to body text. If headings are dramatically larger, --wa-font-size-scale may need to go above 1 (e.g., 1.05). Tight editorial scales go below.

Line height:

  • Dense UIs with narrow leading → --wa-line-height-normal: 1.4. Airy content pages → 1.7+.

Step 4 — Elevation & Shadows

  • Do cards lift off the background? If yes, look for a visible drop shadow.
  • Is the shadow sharp and close (neumorphic) or soft and diffuse (material)?
  • Is there a color tint to the shadow (colored shadows indicate the shadow color is tied to the brand)?
Shadow character--wa-shadow-* direction
Flat (no shadow, borders only)blur-scale: 0, offset-y-scale: 0
Subtle ambientblur-scale: 0.75, offset-y-scale: 0.5
Standard materialblur-scale: 1, offset-y-scale: 1 (default)
Deep dramaticblur-scale: 2, offset-y-scale: 1.5
Directional (hard shadow)blur-scale: 0.25, offset-y-scale: 2

Step 5 — Motion

Motion is rarely visible in static screenshots. Use the textual description if available.

  • “Snappy”, “instant”, “no animation” → --wa-transition-fast: 50ms, --wa-transition-normal: 100ms
  • “Smooth”, “polished” → --wa-transition-slow: 400ms, easing: ease-in-out
  • “Reduced motion” preference → keep defaults; system handles via prefers-reduced-motion

Phase 1 Output

After analysing all screenshots, you should have:

accent_hex: #______ fixed? [y/n]
bg_hex: #______ fixed? [y/n]
bg_light_hex: #______ fixed? [y/n]
text_hex: #______ fixed? [y/n]
text_muted_hex: #____ fixed? [y/n]
border_hex: #______ fixed? [y/n]
dark_mode_present: [y/n]
radius_button: ___px fixed? [y/n]
radius_card: ___px fixed? [y/n]
border_width: ___px fixed? [y/n]
space_density: [tight / default / airy]
font_personality: [system / geometric / humanist / serif / mono]
shadow_character: [flat / subtle / standard / dramatic / directional]
motion_character: [snappy / default / smooth]

Entries marked fixed come from theme.toml and pass through unchanged. Entries marked not fixed are your inferred values for subsequent phases.


Visual Character Quick-Reference

QuestionToken area
What is the primary brand color (hue)?--wa-color-brand-*, palette choice, brand.primary
Light-background or dark-background UI?Surface tokens, light/dark mode strategy
Are backgrounds pure white/black, off-white, or tinted?--wa-color-surface-*, brand.bg
How much contrast do text and backgrounds have?Text + surface tint choices
Are corners sharp, rounded, or pill-shaped?--wa-border-radius-*, geometry.radius_*
Are borders visible on cards/inputs? Thin or prominent?--wa-border-width-*, geometry.border_width
Do cards/dialogs cast shadows? Deep and dramatic or shallow and subtle?--wa-shadow-*, --wa-color-shadow
What typeface personality? (neutral system, friendly sans, editorial serif)--wa-font-family-*
Is text compact (tight leading) or airy?--wa-line-height-*, --wa-space-scale
Are interactive elements bold-weight or regular-weight?--wa-font-weight-action
Does the UI feel dense (small spacing) or spacious?--wa-space-scale, geometry.spacing_gap
Are transitions snappy or smooth?--wa-transition-*
What color signals success, warning, danger? (defaults: green, yellow, red)[semantic] keys

Phase 2 — Color

Color is the most impactful decision. Work top-down: palette → brand → surfaces → text → semantic scales.

2.1 Choose a Color Palette

The palette defines the raw color scale (--wa-color-{hue}-05 through 95) that everything else references.

Palette classCharacter
wa-palette-defaultBalanced, neutral-temperature hues
wa-palette-brightHigher saturation, more vibrant
wa-palette-shoelaceClassic Shoelace legacy hues
Pro: wa-palette-rudimentaryBold primary-leaning colors
Pro: wa-palette-elegantDesaturated, refined palette
Pro: wa-palette-mildSoft, low-contrast palette
Pro: wa-palette-naturalEarthy, organic tones
Pro: wa-palette-anodizedCool metallic palette
Pro: wa-palette-vogueFashion-forward editorial tones

Decision rule: If the reference shows vivid, saturated colors choose bright. If it feels corporate/neutral, use default. If it has an earthy or muted quality, consider a Pro palette.

2.2 Map the Brand Color

Add wa-brand-{hue} to <html> to map a palette hue onto the --wa-color-brand-* semantic scale. Available hues: red, orange, yellow, green, cyan, blue, indigo, purple, pink, gray.

Pick the hue closest to the dominant brand color in the reference screenshots.

2.3 Surface Colors (Light Mode)

Surfaces establish page depth. The scale runs: raised (modals, popovers) → default (main page) → lowered (wells, inset areas).

/* Clean, minimal white UI */
--wa-color-surface-raised: white;
--wa-color-surface-default: white;
--wa-color-surface-lowered: var(--wa-color-neutral-95);
--wa-color-surface-border: var(--wa-color-neutral-90);
/* Warm off-white */
--wa-color-surface-raised: white;
--wa-color-surface-default: var(--wa-color-neutral-95);
--wa-color-surface-lowered: var(--wa-color-neutral-90);
--wa-color-surface-border: var(--wa-color-neutral-80);
/* Tinted brand surface */
--wa-color-surface-raised: white;
--wa-color-surface-default: var(--wa-color-brand-95);
--wa-color-surface-lowered: var(--wa-color-brand-90);
--wa-color-surface-border: var(--wa-color-brand-80);

Decision rule: Count distinct background layers in the screenshots. One flat tone = all three can point to similar tints. Layered UI = spread them across the tint scale.

2.4 Surface Colors (Dark Mode)

Dark mode inverts the elevation logic: higher tint = lighter = closer to the user.

--wa-color-surface-raised: var(--wa-color-neutral-10); /* modals sit above */
--wa-color-surface-default: var(--wa-color-neutral-05); /* page background */
--wa-color-surface-lowered: color-mix(in oklab, var(--wa-color-surface-default), black 20%);
--wa-color-surface-border: var(--wa-color-neutral-20);

For a branded dark mode, replace neutral with the brand hue at low tints (05–20).

2.5 Text Colors

Text must clear 4.5:1 contrast against surfaces (WCAG AA). A tint difference of ≥50 ensures this.

/* Light mode — dark text on light background */
--wa-color-text-normal: var(--wa-color-neutral-20); /* primary text */
--wa-color-text-quiet: var(--wa-color-neutral-40); /* secondary/placeholder */
--wa-color-text-link: var(--wa-color-brand-40); /* links */
/* Dark mode — light text on dark background */
--wa-color-text-normal: var(--wa-color-neutral-95);
--wa-color-text-quiet: var(--wa-color-neutral-60);
--wa-color-text-link: var(--wa-color-brand-70);

Accessibility check: Verify text-normal vs surface-default achieves ≥50 tint difference.

2.6 Semantic Colors

Map the five semantic groups (brand, success, neutral, warning, danger) to hues, then define fill/border/on tokens for quiet/normal/loud attention levels.

Each semantic group needs 9 tokens × 2 modes = 18 token values. The pattern is:

Light mode:
fill-quiet → {hue}-95 (barely there background)
fill-normal → {hue}-90 (subtle background)
fill-loud → {hue}-70 (strong fill, e.g. primary button)
border-quiet → {hue}-70
border-normal → {hue}-50
border-loud → {hue}-30
on-quiet → {hue}-40 (text/icon on quiet fill)
on-normal → {hue}-40
on-loud → text-normal (dark text on light fill)
Dark mode (inverted):
fill-quiet → {hue}-10
fill-normal → {hue}-20
fill-loud → {hue}-50
border-quiet → {hue}-30
border-normal → {hue}-50
border-loud → {hue}-80
on-quiet → {hue}-70
on-normal → {hue}-80
on-loud → white

Deviation: If the screenshot shows white text on a primary button (on-loud), set fill-loud to a mid-range tint (40–60) and on-loud to white. If it shows dark text on a colored button, keep fill-loud at 70–80 and on-loud as text-normal.

2.7 Shadow Color and Overlay Colors

/* Standard: neutral-tinted shadow */
--wa-color-shadow: color-mix(in oklab, var(--wa-color-neutral-05) 12%, transparent);
/* Elevated brand tinting */
--wa-color-shadow: color-mix(in oklab, var(--wa-color-brand-20) 15%, transparent);
/* Overlays */
--wa-color-overlay-modal: color-mix(in oklab, black 50%, transparent); /* backdrop */
--wa-color-overlay-inline: color-mix(in oklab, var(--wa-color-neutral-80) 25%, transparent);

2.8 Interaction Colors

--wa-color-focus: var(--wa-color-brand-60); /* focus ring — keep 3:1 vs surface */
--wa-color-mix-hover: black 10%; /* hover darkening amount */
--wa-color-mix-active: black 20%; /* active/pressed darkening */

For light-colored brand fills that need to lighten on hover, use white instead of black:

--wa-color-mix-hover: white 12%;

Phase 3 — Typography

3.1 Font Families

Identify fonts from the reference. Load them via @import above the @layer block.

@import url('https://fonts.bunny.net/css2?family=Inter:wght@400;500;600&display=swap');
@layer wa-theme {
.wa-theme-{name} { ... }
}

Then set the family tokens:

/* Neutral, system-like */
--wa-font-family-body: 'Inter', ui-sans-serif, sans-serif;
--wa-font-family-heading: var(--wa-font-family-body);
--wa-font-family-code: ui-monospace, monospace;
/* Editorial with distinct heading */
--wa-font-family-body: 'Inter', sans-serif;
--wa-font-family-heading: 'Playfair Display', serif;
--wa-font-family-code: 'JetBrains Mono', monospace;

Decision rule: If the reference shows one font throughout, set heading to var(--wa-font-family-body). Distinct heading personality = separate families.

3.2 Font Size Scale

The scale is auto-calculated from --wa-font-size-m (16px baseline) × ratio 1.125.

/* Compact UI: shrink the baseline */
--wa-font-size-scale: 0.9375; /* ~15px base */
/* Larger/more accessible */
--wa-font-size-scale: 1.0625; /* ~17px base */

Only override individual sizes when the reference has a non-standard type scale.

3.3 Font Weights

/* Standard web font (400/500/600/700 available) */
--wa-font-weight-light: 300;
--wa-font-weight-normal: 400;
--wa-font-weight-semibold: 500;
--wa-font-weight-bold: 600;
/* Variable font shifted heavier */
--wa-font-weight-normal: 450;
--wa-font-weight-semibold: 600;
--wa-font-weight-bold: 700;

Role-based overrides:

--wa-font-weight-body: var(--wa-font-weight-normal);
--wa-font-weight-heading: var(--wa-font-weight-bold);
--wa-font-weight-action: var(--wa-font-weight-semibold); /* buttons, tabs */

Decision rule: If buttons in the screenshot appear heavy/bold, set action to bold. If they look regular, use normal.

3.4 Line Height

/* Compact interface (data-dense) */
--wa-line-height-condensed: 1.2;
--wa-line-height-normal: 1.4;
--wa-line-height-expanded: 1.8;
/* Airy, editorial */
--wa-line-height-condensed: 1.3;
--wa-line-height-normal: 1.7;
--wa-line-height-expanded: 2.2;

Phase 4 — Space

Space controls all internal component padding and gap rhythm throughout the UI.

/* Compact (dense data tables, admin UIs) */
--wa-space-scale: 0.875;
/* Default */
--wa-space-scale: 1;
/* Spacious (marketing, consumer-facing) */
--wa-space-scale: 1.125;

Decision rule: If the screenshot shows tight rows with small gaps, use a scale below 1. If there is generous whitespace around elements, go above 1.


Phase 5 — Borders

5.1 Border Style

--wa-border-style: solid; /* default, most interfaces */
--wa-border-style: dashed; /* technical/developer tools */
--wa-border-style: none; /* flat, borderless style */

5.2 Border Width Scale

/* Hairline, minimal */
--wa-border-width-scale: 0.75;
/* Default (1px/2px/3px) */
--wa-border-width-scale: 1;
/* Thicker, more visible */
--wa-border-width-scale: 1.5;

5.3 Border Radius

This is often the most visually distinctive decision. Observe corner roundness carefully.

/* Sharp, modern/technical (no rounding) */
--wa-border-radius-scale: 0;
--wa-border-radius-s: 0;
--wa-border-radius-m: 0;
--wa-border-radius-l: 0;
/* Slightly rounded (enterprise, professional) */
--wa-border-radius-s: 2px;
--wa-border-radius-m: 4px;
--wa-border-radius-l: 8px;
/* Default (3px / 6px / 12px) */
--wa-border-radius-scale: 1;
/* Friendly, consumer-facing */
--wa-border-radius-s: 4px;
--wa-border-radius-m: 8px;
--wa-border-radius-l: 16px;
/* Pill-heavy (very rounded) */
--wa-border-radius-s: 6px;
--wa-border-radius-m: 12px;
--wa-border-radius-l: 24px;

Use --wa-border-radius-scale for proportional global adjustment, or set individual tokens for more control.


Phase 6 — Shadows

Shadows convey elevation depth and, optionally, interactivity.

6.1 Shadow Intensity

Control overall darkness via --wa-color-shadow (in Phase 2.7), and intensity/spread via scale properties.

/* Flat UI — no shadows */
--wa-shadow-s: none;
--wa-shadow-m: none;
--wa-shadow-l: none;
/* Subtle (default-like) */
--wa-shadow-blur-scale: 1;
--wa-shadow-offset-y-scale: 1;
--wa-shadow-spread-scale: -0.5;
/* More prominent, material-like */
--wa-shadow-blur-scale: 2;
--wa-shadow-offset-y-scale: 1.5;
--wa-shadow-spread-scale: 0;
/* Dramatic */
--wa-shadow-blur-scale: 3;
--wa-shadow-offset-y-scale: 2;

6.2 Directional Shadows

Add horizontal offset for a light-source-from-top-left effect:

--wa-shadow-offset-x-scale: 0.3; /* slight right shadow */

Default is 0 (symmetrical drop shadow).


Phase 7 — Focus

The focus ring is critical for keyboard accessibility. Use a color that contrasts 3:1 against both light and dark surfaces.

--wa-color-focus: var(--wa-color-brand-60); /* uses brand mid-range */
--wa-focus-ring-style: solid;
--wa-focus-ring-width: 3px; /* 0.1875rem */
--wa-focus-ring-offset: 1px; /* gap between element and ring */

For a thicker, high-visibility ring:

--wa-focus-ring-width: 4px;
--wa-focus-ring-offset: 2px;

For a subtle, thin ring:

--wa-focus-ring-width: 2px;
--wa-focus-ring-offset: 0px;

Phase 8 — Transitions

Match the animation feel to the UI personality.

/* Snappy, responsive (data/admin tools) */
--wa-transition-fast: 50ms;
--wa-transition-normal: 100ms;
--wa-transition-slow: 200ms;
--wa-transition-easing: ease-out;
/* Default */
--wa-transition-fast: 75ms;
--wa-transition-normal: 150ms;
--wa-transition-slow: 300ms;
--wa-transition-easing: ease;
/* Smooth, polished (consumer apps) */
--wa-transition-fast: 100ms;
--wa-transition-normal: 200ms;
--wa-transition-slow: 400ms;
--wa-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
/* Disable all motion (reduced-motion compliance) */
--wa-transition-fast: 0ms;
--wa-transition-normal: 0ms;
--wa-transition-slow: 0ms;

Phase 9 — Component Groups

These tokens tune groups of related components simultaneously.

9.1 Form Controls

Controls inputs, selects, textareas, checkboxes, radios, sliders, buttons.

/* Default form control appearance */
--wa-form-control-background-color: var(--wa-color-surface-default);
--wa-form-control-border-color: var(--wa-color-neutral-border-loud);
--wa-form-control-border-width: var(--wa-border-width-s);
--wa-form-control-border-radius: var(--wa-border-radius-m);
--wa-form-control-activated-color: var(--wa-color-brand-fill-loud); /* checked state */
/* Text inside controls */
--wa-form-control-label-color: var(--wa-color-neutral-border-loud);
--wa-form-control-label-font-weight: var(--wa-font-weight-normal);
--wa-form-control-value-color: var(--wa-color-text-normal);
--wa-form-control-placeholder-color: var(--wa-color-gray-60);
/* Padding — affects control height */
--wa-form-control-padding-block: 0.75em;
--wa-form-control-padding-inline: 1em;

Compact controls:

--wa-form-control-padding-block: 0.5em;
--wa-form-control-padding-inline: 0.75em;

Larger touch targets:

--wa-form-control-padding-block: 1em;
--wa-form-control-padding-inline: 1.25em;

9.2 Panels

Controls cards, callouts, details, dialogs — anything with a larger contained surface.

--wa-panel-border-style: var(--wa-border-style);
--wa-panel-border-width: var(--wa-border-width-s); /* 1px */
--wa-panel-border-radius: var(--wa-border-radius-l); /* 12px */

Flat cards (no border):

--wa-panel-border-width: 0;

Rounder panels:

--wa-panel-border-radius: 1.5rem;

9.3 Tooltips

--wa-tooltip-background-color: var(--wa-color-neutral-fill-loud);
--wa-tooltip-content-color: var(--wa-color-neutral-on-loud);
--wa-tooltip-border-radius: var(--wa-border-radius-s);
--wa-tooltip-font-size: var(--wa-font-size-s);

Dark branded tooltips:

--wa-tooltip-background-color: var(--wa-color-brand-fill-loud);
--wa-tooltip-content-color: var(--wa-color-brand-on-loud);
--wa-tooltip-border-color: var(--wa-color-brand-fill-loud);

Phase 10 — Verify

Contrast Checklist

  • --wa-color-text-normal vs --wa-color-surface-default → ≥4.5:1 (WCAG AA)
  • --wa-color-text-quiet vs --wa-color-surface-default → ≥3:1 (for non-essential text)
  • --wa-color-brand-on-loud vs --wa-color-brand-fill-loud → ≥4.5:1
  • --wa-color-focus vs --wa-color-surface-default → ≥3:1

Tint arithmetic shortcut: A difference of ≥50 on the WA tint scale reliably achieves 4.5:1. A difference of ≥40 achieves 3:1.

Light/Dark Parity Checklist

  • All surface tokens defined for both modes
  • All text tokens defined for both modes
  • All semantic fill/border/on tokens defined for both modes
  • Shadow color color-mix opacity adjusted for dark (needs higher % opacity)
  • Overlay modal opacity remains readable over dark surfaces

Visual Smoke Test

Check these component types against the reference screenshots:

  1. Primary button — brand color, correct text weight, hover/active state
  2. Text input — border, label, placeholder, focus ring
  3. Card — surface, border, shadow, radius
  4. Callout — semantic fill colors for all variants (brand/success/warning/danger)
  5. Dialog — overlay, raised surface, shadow
  6. Badge/Tag — semantic fill + border + on colors at quiet/normal/loud

Starter Theme Template

/* ============================================================
Theme: {name}
Based on: {reference description}
Created: {date}
============================================================ */
/* Optional: load custom fonts above the @layer block */
/* @import url('...'); */
@layer wa-theme {
/* ---- LIGHT MODE ----------------------------------------- */
.wa-theme-{name},
.wa-theme-{name}.wa-light,
.wa-theme-{name} .wa-light,
.wa-theme-{name}.wa-dark .wa-invert,
.wa-theme-{name} .wa-dark .wa-invert,
.wa-light .wa-theme-{name},
.wa-dark .wa-theme-{name}.wa-invert,
.wa-dark .wa-theme-{name} .wa-invert {
color-scheme: light;
color: var(--wa-color-text-normal);
/* Surfaces */
--wa-color-surface-raised: white;
--wa-color-surface-default: white;
--wa-color-surface-lowered: var(--wa-color-neutral-95);
--wa-color-surface-border: var(--wa-color-neutral-90);
/* Text */
--wa-color-text-normal: var(--wa-color-neutral-20);
--wa-color-text-quiet: var(--wa-color-neutral-40);
--wa-color-text-link: var(--wa-color-brand-40);
/* Overlays */
--wa-color-overlay-modal: color-mix(in oklab, black 50%, transparent);
--wa-color-overlay-inline: color-mix(in oklab, var(--wa-color-neutral-80) 25%, transparent);
/* Shadow */
--wa-color-shadow: color-mix(in oklab, var(--wa-color-neutral-05) 12%, transparent);
/* Interactions */
--wa-color-focus: var(--wa-color-brand-60);
--wa-color-mix-hover: black 10%;
--wa-color-mix-active: black 20%;
/* Semantic — Brand */
--wa-color-brand-fill-quiet: var(--wa-color-brand-95);
--wa-color-brand-fill-normal: var(--wa-color-brand-90);
--wa-color-brand-fill-loud: var(--wa-color-brand-70);
--wa-color-brand-border-quiet: var(--wa-color-brand-70);
--wa-color-brand-border-normal: var(--wa-color-brand-50);
--wa-color-brand-border-loud: var(--wa-color-brand-30);
--wa-color-brand-on-quiet: var(--wa-color-brand-40);
--wa-color-brand-on-normal: var(--wa-color-brand-40);
--wa-color-brand-on-loud: var(--wa-color-text-normal);
/* Semantic — Success */
--wa-color-success-fill-quiet: var(--wa-color-success-95);
--wa-color-success-fill-normal: var(--wa-color-success-90);
--wa-color-success-fill-loud: var(--wa-color-success-80);
--wa-color-success-border-quiet: var(--wa-color-success-70);
--wa-color-success-border-normal: var(--wa-color-success-50);
--wa-color-success-border-loud: var(--wa-color-success-30);
--wa-color-success-on-quiet: var(--wa-color-success-40);
--wa-color-success-on-normal: var(--wa-color-success-40);
--wa-color-success-on-loud: var(--wa-color-text-normal);
/* Semantic — Warning */
--wa-color-warning-fill-quiet: var(--wa-color-warning-95);
--wa-color-warning-fill-normal: var(--wa-color-warning-90);
--wa-color-warning-fill-loud: var(--wa-color-warning-80);
--wa-color-warning-border-quiet: var(--wa-color-warning-70);
--wa-color-warning-border-normal: var(--wa-color-warning-50);
--wa-color-warning-border-loud: var(--wa-color-warning-30);
--wa-color-warning-on-quiet: var(--wa-color-warning-40);
--wa-color-warning-on-normal: var(--wa-color-warning-40);
--wa-color-warning-on-loud: var(--wa-color-text-normal);
/* Semantic — Danger */
--wa-color-danger-fill-quiet: var(--wa-color-danger-95);
--wa-color-danger-fill-normal: var(--wa-color-danger-90);
--wa-color-danger-fill-loud: var(--wa-color-danger-70);
--wa-color-danger-border-quiet: var(--wa-color-danger-70);
--wa-color-danger-border-normal: var(--wa-color-danger-50);
--wa-color-danger-border-loud: var(--wa-color-danger-30);
--wa-color-danger-on-quiet: var(--wa-color-danger-40);
--wa-color-danger-on-normal: var(--wa-color-danger-40);
--wa-color-danger-on-loud: var(--wa-color-text-normal);
/* Semantic — Neutral */
--wa-color-neutral-fill-quiet: var(--wa-color-neutral-95);
--wa-color-neutral-fill-normal: var(--wa-color-neutral-90);
--wa-color-neutral-fill-loud: var(--wa-color-neutral-80);
--wa-color-neutral-border-quiet: var(--wa-color-neutral-70);
--wa-color-neutral-border-normal: var(--wa-color-neutral-50);
--wa-color-neutral-border-loud: var(--wa-color-neutral-30);
--wa-color-neutral-on-quiet: var(--wa-color-neutral-40);
--wa-color-neutral-on-normal: var(--wa-color-neutral-40);
--wa-color-neutral-on-loud: var(--wa-color-text-normal);
}
/* ---- DARK MODE ------------------------------------------ */
.wa-theme-{name}.wa-dark,
.wa-theme-{name} .wa-dark,
.wa-theme-{name}.wa-invert,
.wa-theme-{name} .wa-invert,
.wa-dark .wa-theme-{name},
.wa-light .wa-theme-{name}.wa-invert,
.wa-light .wa-theme-{name} .wa-invert {
color-scheme: dark;
color: var(--wa-color-text-normal);
/* Surfaces */
--wa-color-surface-raised: var(--wa-color-neutral-10);
--wa-color-surface-default: var(--wa-color-neutral-05);
--wa-color-surface-lowered: color-mix(in oklab, var(--wa-color-surface-default), black 20%);
--wa-color-surface-border: var(--wa-color-neutral-20);
/* Text */
--wa-color-text-normal: var(--wa-color-neutral-95);
--wa-color-text-quiet: var(--wa-color-neutral-60);
--wa-color-text-link: var(--wa-color-brand-70);
/* Overlays */
--wa-color-overlay-modal: color-mix(in oklab, black 60%, transparent);
--wa-color-overlay-inline: color-mix(in oklab, var(--wa-color-neutral-50) 10%, transparent);
/* Shadow */
--wa-color-shadow: color-mix(in oklab, black 40%, transparent);
/* Interactions */
--wa-color-focus: var(--wa-color-brand-60);
--wa-color-mix-hover: black 8%;
--wa-color-mix-active: black 16%;
/* Semantic — Brand */
--wa-color-brand-fill-quiet: var(--wa-color-brand-10);
--wa-color-brand-fill-normal: var(--wa-color-brand-20);
--wa-color-brand-fill-loud: var(--wa-color-brand-50);
--wa-color-brand-border-quiet: var(--wa-color-brand-30);
--wa-color-brand-border-normal: var(--wa-color-brand-50);
--wa-color-brand-border-loud: var(--wa-color-brand-80);
--wa-color-brand-on-quiet: var(--wa-color-brand-70);
--wa-color-brand-on-normal: var(--wa-color-brand-80);
--wa-color-brand-on-loud: white;
/* Semantic — Success */
--wa-color-success-fill-quiet: var(--wa-color-success-10);
--wa-color-success-fill-normal: var(--wa-color-success-20);
--wa-color-success-fill-loud: var(--wa-color-success-50);
--wa-color-success-border-quiet: var(--wa-color-success-30);
--wa-color-success-border-normal: var(--wa-color-success-50);
--wa-color-success-border-loud: var(--wa-color-success-80);
--wa-color-success-on-quiet: var(--wa-color-success-70);
--wa-color-success-on-normal: var(--wa-color-success-80);
--wa-color-success-on-loud: white;
/* Semantic — Warning */
--wa-color-warning-fill-quiet: var(--wa-color-warning-10);
--wa-color-warning-fill-normal: var(--wa-color-warning-20);
--wa-color-warning-fill-loud: var(--wa-color-warning-70);
--wa-color-warning-border-quiet: var(--wa-color-warning-30);
--wa-color-warning-border-normal: var(--wa-color-warning-50);
--wa-color-warning-border-loud: var(--wa-color-warning-80);
--wa-color-warning-on-quiet: var(--wa-color-warning-70);
--wa-color-warning-on-normal: var(--wa-color-warning-80);
--wa-color-warning-on-loud: var(--wa-color-warning-05);
/* Semantic — Danger */
--wa-color-danger-fill-quiet: var(--wa-color-danger-10);
--wa-color-danger-fill-normal: var(--wa-color-danger-20);
--wa-color-danger-fill-loud: var(--wa-color-danger-50);
--wa-color-danger-border-quiet: var(--wa-color-danger-30);
--wa-color-danger-border-normal: var(--wa-color-danger-50);
--wa-color-danger-border-loud: var(--wa-color-danger-80);
--wa-color-danger-on-quiet: var(--wa-color-danger-70);
--wa-color-danger-on-normal: var(--wa-color-danger-80);
--wa-color-danger-on-loud: white;
/* Semantic — Neutral */
--wa-color-neutral-fill-quiet: var(--wa-color-neutral-10);
--wa-color-neutral-fill-normal: var(--wa-color-neutral-20);
--wa-color-neutral-fill-loud: var(--wa-color-neutral-50);
--wa-color-neutral-border-quiet: var(--wa-color-neutral-30);
--wa-color-neutral-border-normal: var(--wa-color-neutral-50);
--wa-color-neutral-border-loud: var(--wa-color-neutral-80);
--wa-color-neutral-on-quiet: var(--wa-color-neutral-70);
--wa-color-neutral-on-normal: var(--wa-color-neutral-80);
--wa-color-neutral-on-loud: white;
}
/* ---- SHARED (light + dark) ------------------------------- */
.wa-theme-{name},
.wa-theme-{name}.wa-light,
.wa-theme-{name} .wa-light,
.wa-theme-{name}.wa-dark,
.wa-theme-{name} .wa-dark {
/* Typography */
--wa-font-family-body: ui-sans-serif, system-ui, sans-serif;
--wa-font-family-heading: var(--wa-font-family-body);
--wa-font-family-code: ui-monospace, monospace;
--wa-font-size-scale: 1;
--wa-font-weight-normal: 400;
--wa-font-weight-semibold: 500;
--wa-font-weight-bold: 600;
--wa-font-weight-body: var(--wa-font-weight-normal);
--wa-font-weight-heading: var(--wa-font-weight-bold);
--wa-font-weight-action: var(--wa-font-weight-semibold);
--wa-line-height-condensed: 1.2;
--wa-line-height-normal: 1.6;
--wa-line-height-expanded: 2;
/* Space */
--wa-space-scale: 1;
/* Borders */
--wa-border-style: solid;
--wa-border-radius-s: 0.1875rem;
--wa-border-radius-m: 0.375rem;
--wa-border-radius-l: 0.75rem;
/* Shadows */
--wa-shadow-offset-y-scale: 1;
--wa-shadow-blur-scale: 1;
--wa-shadow-spread-scale: -0.5;
/* Focus */
--wa-focus-ring-style: solid;
--wa-focus-ring-width: 0.1875rem;
--wa-focus-ring-offset: 0.0625rem;
/* Transitions */
--wa-transition-fast: 75ms;
--wa-transition-normal: 150ms;
--wa-transition-slow: 300ms;
--wa-transition-easing: ease;
/* Component groups */
--wa-form-control-border-radius: var(--wa-border-radius-m);
--wa-panel-border-radius: var(--wa-border-radius-l);
}
}

Iterative Refinement

After the initial pass, iterate by area:

Color iteration

  • Adjust surface tints until the background feels right (too stark → add tint; too murky → lighten)
  • Tweak fill-loud and on-loud together — they must stay accessible as a pair
  • If dark mode feels too flat, increase the gap between surface-raised and surface-default tints

Shape iteration

  • Start with border-radius-scale for global adjustments
  • Fine-tune form-control-border-radius and panel-border-radius independently if forms and cards need different corner styles
  • Remove all borders (panel-border-width: 0) to test a shadow-only depth model

Typography iteration

  • Adjust font-weight-action first — it affects buttons, tabs, and nav items most visibly
  • Shift font-size-scale by ±0.0625 per pass until text feels appropriately sized
  • Test line-height-normal with multi-line body text, not just labels

Density iteration

  • space-scale is a blunt instrument — use it first, then override specific components if needed
  • For data-dense UIs, reducing form-control-padding-block from 0.75em to 0.5em is often sufficient without touching global space

Motion iteration

  • Halve all durations if interactions feel sluggish
  • Switch easing from ease to ease-out for a snappier feel on open/close animations