Authoring Wireframe Themes
Wireframe themes control the visual appearance of the low-fidelity UI preview area — the palette, radii, and typography that make a wf-* component look like Material, iOS, or Tamagui. They are separate from Web Awesome site themes (--wa-* tokens); do not confuse the two layers.
What a wireframe theme controls
--wf-color-*variables — brand, background, text, border, and semantic colors for allwf-*components--wf-radius-*variables — corner radii from buttons to modals--wf-font-*variables — typeface family for body, headings, and code--wf-spacing-gap— base rhythm for stacks and rows- Optional component overrides —
.wf-{component}class rules scoped to the theme
The wf-* components in the wireframe preview area are intentionally low-fidelity. Do not replace them with wa-* components.
Active themes
| ID | Label | CSS file | Light | Dark |
|---|---|---|---|---|
"" | Sketch | wireframe.css (base, always loaded) | ✅ | ✅ |
mui | Material | wireframe-mui.css | ✅ | ✅ |
ios | iOS | wireframe-ios.css | ✅ | ✅ |
tamagui | Tamagui | wireframe-tamagui.css | ✅ | ✅ |
How dark/light mode works
The site runs three co-operating axes simultaneously:
Starlight: data-theme="light"|"dark" on <html> (user-toggled) ↓ Head.astro bridge (inline script, MutationObserver)WA: .wa-light / .wa-dark on <html> (synced from Starlight) independent ↓Wireframe: data-wf-theme="{id}" on <html> (ThemeDropdown → localStorage)Both wa-dark/wa-light and data-wf-theme live on the same <html> element, making compound selectors the correct pattern. The bridge in Head.astro is correct as-is — no modifications needed.
Selector pattern
Both wa-dark/wa-light and data-wf-theme are set on the same <html> element. Themes must define variables in two compound selector blocks so they cooperate with Web Awesome’s dark/light class mechanism:
/* ── Light (default) ──────────────────────────────────────── *//* Applies when wa-light is present, and also before any WA *//* class is set (prevents flash of unstyled content at paint). */:root[data-wf-theme="{id}"],:root.wa-light[data-wf-theme="{id}"] { --wf-color-primary: #…; --wf-color-primary-text: #…; --wf-color-bg: #…; --wf-color-bg-light: #…; --wf-color-bg-elevated: #…; --wf-color-text: #…; --wf-color-text-muted: #…; --wf-color-border: #…; --wf-color-border-dark: #…; --wf-color-success: #…; --wf-color-warning: #…; --wf-color-danger: #…; --wf-radius-card: …px; --wf-radius-button: …px; --wf-border-width: 1px; --wf-spacing-gap: 1rem; /* Optional: --wf-font-body, --wf-font-heading, --wf-font-mono */}
/* ── Dark ─────────────────────────────────────────────────── *//* Overrides when wa-dark is present on <html>. */:root.wa-dark[data-wf-theme="{id}"] { --wf-color-primary: #…; /* often a lighter tint than light mode */ --wf-color-bg: #…; --wf-color-bg-light: #…; --wf-color-bg-elevated: #…; --wf-color-text: #…; --wf-color-text-muted: #…; --wf-color-border: #…; --wf-color-border-dark: #…;}
/* ── Component overrides ─────────────────────────────────── *//* Scope to the theme to avoid polluting other themes. *//* Use :root[data-wf-theme="{id}"] .wf-{component} for *//* overrides that apply in both modes; narrow further with *//* .wa-light / .wa-dark when mode-specific shapes are needed. */:root[data-wf-theme="{id}"] .wf-card { … }:root.wa-dark[data-wf-theme="{id}"] .wf-card { … }Specificity notes
| Selector | Specificity | When it wins |
|---|---|---|
:root[data-wf-theme="{id}"] | (0,1,1) | Default / light mode |
:root.wa-light[data-wf-theme="{id}"] | (0,2,1) | Explicit light mode (same result) |
:root.wa-dark[data-wf-theme="{id}"] | (0,2,1) | Dark mode — beats the single-selector default |
The dark block wins by specificity alone — no !important or load-order tricks required. The plain :root[data-wf-theme] block handles the moment before any WA class is applied (pre-paint flash prevention).
Dark palette guidelines
When authoring dark-mode values, follow the design system’s own dark palette rather than inventing arbitrary darks:
| Theme | Light primary | Dark primary | Dark bg | Dark surface | Dark text |
|---|---|---|---|---|---|
| Sketch | #4a5568 | #718096 | #1a1e2e | #252b3b | #e2e8f0 |
| Material | #1976d2 | #90caf9 | #121212 | #1e1e1e | rgba(255,255,255,0.87) |
| iOS | #007aff | #0a84ff | #000000 | #1c1c1e | #ffffff |
| Tamagui | #3a9e50 | #32d74b | #111111 | #1a1a1a | #ebebeb |
Penpot / already-dark designs: If the source design is inherently dark, treat its dark values as the dark block and infer a light variant. Do not hard-code dark backgrounds with no light alternative — this creates a clash when the user has Starlight set to light mode.
Required variables
Every StarSpec wireframe theme defines CSS variables in two scope blocks — one for light mode and one for dark mode:
/* Light (default) */:root[data-wf-theme="id"],:root.wa-light[data-wf-theme="id"] { … }
/* Dark */:root.wa-dark[data-wf-theme="id"] { … }Variables marked required must be defined in both blocks. Variables marked optional extend the base and are only needed when a theme diverges from the Sketch defaults.
Brand Colors (required)
| Variable | Description | Typical Light | Typical Dark |
|---|---|---|---|
--wf-color-primary | Main accent — buttons, active states, focus rings | #5E6AD2 | lighter tint of light value |
--wf-color-primary-text | Text on primary background (must contrast primary) | #FFFFFF | #000000 or #FFFFFF |
--wf-color-bg | Page/wireframe background | #FFFFFF | #111111–#1E1E1E |
--wf-color-bg-light | Card and input field background | #F7FAFC | slightly lighter than bg |
--wf-color-bg-elevated | Active segment, hover, raised surface | #FFFFFF | slightly lighter than bg-light |
--wf-color-text | Primary text color | #1A202C | rgba(255,255,255,0.87) |
--wf-color-text-muted | Secondary text, hints, placeholders | #718096 | rgba(255,255,255,0.60) |
--wf-color-border | Default container border | #E2E8F0 | rgba(255,255,255,0.12) |
--wf-color-border-dark | Interactive element border (inputs, focus) | #CBD5E0 | rgba(255,255,255,0.23) |
Semantic Colors (required)
| Variable | Description | Typical Value |
|---|---|---|
--wf-color-success | Success states, progress bars | #38A169 |
--wf-color-warning | Warning states, progress bars | #DD6B20 |
--wf-color-danger | Error states, destructive actions | #E53E3E |
Extended Semantic (optional)
| Variable | Description | Usage |
|---|---|---|
--wf-color-danger-bg | Subtle danger background fill | Danger callout rows |
--wf-color-danger-border | Danger border for callout containers | Borders on danger cards |
Geometry & Spacing (required)
| Variable | Description | Typical Value |
|---|---|---|
--wf-radius-card | Corner radius for cards and modals | 12px |
--wf-radius-button | Corner radius for buttons and inputs | 8px |
--wf-border-width | Thickness of borders | 1px |
--wf-spacing-gap | Base gap for stacks and rows | 1rem |
Extended Radii (optional)
Use when a theme needs component-level radius control beyond the two base values.
| Variable | Description | Typical Value |
|---|---|---|
--wf-radius-sm | Small radius (badges, tags) | 4px |
--wf-radius-md | Medium radius (inputs, buttons) | 8px |
--wf-radius-lg | Large radius (cards, modals) | 12px |
--wf-radius-pill | Full pill (toggle buttons, chips) | 100px |
--wf-radius-input | Input field radius | 6px |
--wf-radius-modal | Modal/dialog radius | 12px |
--wf-radius-badge | Badge/chip radius | 4px |
--wf-radius-tabs | Tab container radius | 8px |
Font (optional)
Override fonts when the design system uses a custom typeface. Load the font family via @import above the variable blocks.
| Variable | Description | Default |
|---|---|---|
--wf-font-body | Body and label text | inherit (Starlight site font) |
--wf-font-heading | Heading elements | inherit |
--wf-font-mono | Code, values, monospaced text | ui-monospace, monospace |
Step-by-step: creating a new theme
1. Optional — create src/styles/{id}.theme.toml
A theme.toml anchor file lets you declare known values that StarVision must respect during visual extraction. See Theme TOML Specification for the schema. Partial files are valid — only declare what you know.
[theme]id = "my-theme"label = "My Theme"
[brand]primary = "#5E6AD2"bg = "#FFFFFF"
[brand.dark]primary = "#818CF8"bg = "#111111"
[geometry]radius_card = "12px"radius_button = "8px"2. Create src/styles/wireframe-{id}.css
Use the selector template above. Required checklist:
- Light block: all
--wf-color-*required variables defined - Dark block: all
--wf-color-*required variables defined (typically inverted values) - Both blocks:
--wf-radius-card,--wf-radius-button,--wf-border-width,--wf-spacing-gap - Font
@importabove the variable blocks if[font]is declared in theme.toml - No hardcoded hex values inside component rules — reference
--wf-*variables only - Component overrides scoped with
:root[data-wf-theme="{id}"] .wf-{component}
3. Register in src/themes.json
{ "id": "{id}", "label": "{Label}" }astro.config.ts auto-discovers wireframe-{id}.css via fs.existsSync(). No manual config change needed. The theme will appear in the header dropdown immediately.
4. Verify
- Toggle Starlight to dark with the new theme active — wireframe area must update
- Toggle Starlight to light — wireframe must return to light values (no hardcoded dark remains)
- Select the new theme, then switch to each other theme — no residual variables from the new theme
- Run
pnpm compile— no validation errors - Check the header dropdown lists the new theme with the correct label
Using --sl-color-* tokens in theme CSS
Several theme files use Starlight’s --sl-color-* tokens inside wf-* rules. This is intentional only when the goal is to match the page chrome (e.g., a hairline card border that blends with the Starlight sidebar). Use --wf-color-* variables whenever the intent is to represent the design system being previewed, not the documentation site surrounding it.