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 sluglabel = "My Theme" # Human-readable name
[brand]# Light mode values — map to --wf-color-* wireframe variablesprimary = "#5E6AD2" # Main accent (buttons, active states)primary_text = "#FFFFFF" # Text on primary bg (must contrast primary)bg = "#FFFFFF" # Page backgroundbg_light = "#F7FAFC" # Card / input field backgroundbg_elevated = "#FFFFFF" # Hover state / active segment backgroundtext = "#1A202C" # Primary texttext_muted = "#718096" # Secondary / hint textborder = "#E2E8F0" # Container bordersborder_dark = "#CBD5E0" # Interactive element borders
[brand.dark]# Dark mode overrides — only specify what differsprimary = "#818CF8"bg = "#111111"text = "#EBEBEB"
[semantic]success = "#38A169"warning = "#DD6B20"danger = "#E53E3E"
[geometry]radius_card = "12px" # --wf-radius-cardradius_button = "8px" # --wf-radius-buttonborder_width = "1px" # --wf-border-widthspacing_gap = "1rem" # --wf-spacing-gapVariable 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 key | CSS variable | Role in WA theme |
|---|---|---|
brand.primary | --wf-color-primary | Drives --wa-color-brand-fill-loud |
brand.primary_text | --wf-color-primary-text | Drives --wa-color-brand-on-loud |
brand.bg | --wf-color-bg | Maps to --wa-color-surface-default |
brand.bg_light | --wf-color-bg-light | Maps to --wa-color-surface-lowered |
brand.bg_elevated | --wf-color-bg-elevated | Maps to --wa-color-surface-raised |
brand.text | --wf-color-text | Maps to --wa-color-text-normal |
brand.text_muted | --wf-color-text-muted | Maps to --wa-color-text-quiet |
brand.border | --wf-color-border | Maps to --wa-color-surface-border |
brand.border_dark | --wf-color-border-dark | Maps to panel/form control borders |
semantic.success | --wf-color-success | Maps to --wa-color-success-fill-loud |
semantic.warning | --wf-color-warning | Maps to --wa-color-warning-fill-loud |
semantic.danger | --wf-color-danger | Maps to --wa-color-danger-fill-loud |
geometry.radius_card | --wf-radius-card | Maps to --wa-panel-border-radius |
geometry.radius_button | --wf-radius-button | Maps to --wa-form-control-border-radius |
geometry.border_width | --wf-border-width | Maps to --wa-border-width-m |
geometry.spacing_gap | --wf-spacing-gap | Informs --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.
- Read theme.toml — collect explicit anchor values; mark them as fixed
- Analyse screenshots — extract design intent from visual references
- Color — palette, brand, surfaces, text, semantic colors
- Typography — fonts, size scale, weights, line height
- Space — density/rhythm
- Borders — corner style, stroke weight
- Shadows — elevation character
- Focus — accessibility ring
- Transitions — motion speed and feel
- Component groups — form controls, panels, tooltips
- Verify — light/dark parity, accessibility contrast, visual smoke test
Phase 0 — Read theme.toml
Before examining screenshots, parse theme.toml if provided:
- Extract every declared key and note its CSS variable and value.
- Mark each as fixed — do not infer or override these from screenshots.
- Note which keys are absent — these must be inferred in Phase 1.
- 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 impression | Approximate values |
|---|---|
| Sharp / flat | radius_button: "2px", radius_card: "4px" |
| Professional | radius_button: "6px", radius_card: "8px" |
| Default friendly | radius_button: "8px", radius_card: "12px" |
| Rounded / modern | radius_button: "12px", radius_card: "16px" |
| Pill buttons | radius_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-scalemay need to go above1(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 ambient | blur-scale: 0.75, offset-y-scale: 0.5 |
| Standard material | blur-scale: 1, offset-y-scale: 1 (default) |
| Deep dramatic | blur-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
| Question | Token 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 class | Character |
|---|---|
wa-palette-default | Balanced, neutral-temperature hues |
wa-palette-bright | Higher saturation, more vibrant |
wa-palette-shoelace | Classic Shoelace legacy hues |
Pro: wa-palette-rudimentary | Bold primary-leaning colors |
Pro: wa-palette-elegant | Desaturated, refined palette |
Pro: wa-palette-mild | Soft, low-contrast palette |
Pro: wa-palette-natural | Earthy, organic tones |
Pro: wa-palette-anodized | Cool metallic palette |
Pro: wa-palette-vogue | Fashion-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 → whiteDeviation: 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-normalvs--wa-color-surface-default→ ≥4.5:1 (WCAG AA)--wa-color-text-quietvs--wa-color-surface-default→ ≥3:1 (for non-essential text)--wa-color-brand-on-loudvs--wa-color-brand-fill-loud→ ≥4.5:1--wa-color-focusvs--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-mixopacity 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:
- Primary button — brand color, correct text weight, hover/active state
- Text input — border, label, placeholder, focus ring
- Card — surface, border, shadow, radius
- Callout — semantic fill colors for all variants (brand/success/warning/danger)
- Dialog — overlay, raised surface, shadow
- 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-loudandon-loudtogether — they must stay accessible as a pair - If dark mode feels too flat, increase the gap between
surface-raisedandsurface-defaulttints
Shape iteration
- Start with
border-radius-scalefor global adjustments - Fine-tune
form-control-border-radiusandpanel-border-radiusindependently 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-actionfirst — it affects buttons, tabs, and nav items most visibly - Shift
font-size-scaleby ±0.0625 per pass until text feels appropriately sized - Test
line-height-normalwith multi-line body text, not just labels
Density iteration
space-scaleis a blunt instrument — use it first, then override specific components if needed- For data-dense UIs, reducing
form-control-padding-blockfrom0.75emto0.5emis often sufficient without touching global space
Motion iteration
- Halve all durations if interactions feel sluggish
- Switch
easingfromeasetoease-outfor a snappier feel on open/close animations