VisorVisor
Themes

Creating Themes

How to create a Visor theme — the theme contract, 5-section template, standard vs creative themes, and the space theme as a reference implementation.

Visor themes are CSS files that override the design tokens from @loworbitstudio/visor-core. Any valid theme transforms every Visor component's appearance without touching component code.

Theme Contract

The theme contract defines which tokens a theme must override, which are optional, and how to add theme-specific extensions.

Required Tokens (~35)

A theme must override all of these for components to render correctly in both light and dark modes.

Text:

TokenPurpose
--text-primaryPrimary body text
--text-secondarySupporting text
--text-tertiaryPlaceholder text
--text-disabledDisabled state
--text-inverseText on inverted backgrounds
--text-inverse-secondarySecondary text on inverted backgrounds
--text-linkLink text
--text-link-hoverLink hover
--text-successSuccess feedback
--text-warningWarning feedback
--text-errorError feedback
--text-infoInfo feedback

Surface:

TokenPurpose
--surface-pagePage background
--surface-cardCard/panel background
--surface-subtleSubtle differentiation
--surface-mutedMuted areas
--surface-overlayOverlay base color
--surface-interactive-defaultInteractive resting state
--surface-interactive-hoverInteractive hover
--surface-interactive-activeInteractive pressed
--surface-selectedPersistent selected state (active nav item, currently-selected list row)

Border:

TokenPurpose
--border-defaultStandard borders
--border-mutedSubtle borders
--border-strongEmphasized borders
--border-focusFocus ring color
--border-disabledDisabled borders

Interactive:

TokenPurpose
--interactive-primary-bgPrimary button background
--interactive-primary-bg-hoverPrimary button hover
--interactive-primary-textPrimary button text
--interactive-secondary-bgSecondary button background
--interactive-secondary-bg-hoverSecondary button hover
--interactive-secondary-textSecondary button text
--interactive-destructive-bgDestructive button background
--interactive-destructive-bg-hoverDestructive button hover
--interactive-destructive-textDestructive button text

Optional Tokens (~15)

These have sensible defaults from visor-core. Override them to customize accent and status colors.

Includes: --surface-accent-subtle, --surface-accent-default, --surface-accent-strong, all status surfaces (success, warning, error, info in subtle and default variants), and status borders.

Letter-spacing ramp

The engine emits a 6-tier letter-spacing ramp from the theme's typography.letter-spacing YAML block, falling back to the Visor default ramp per tier:

TokenDefaultNotes
--letter-spacing-xl0.16emWidest — eyebrows / overlines, uppercase
--letter-spacing-lg0.1em
--letter-spacing-md0.05emAnchor — body / default
--letter-spacing-sm0.025em
--letter-spacing-xs0.01em
--letter-spacing-tight-0.01emTightest — large display headings
typography:
  letter-spacing:
    xl: "3.5rem" # any subset of tiers; omitted tiers use the default ramp
    tight: "0.03rem"

The legacy triad keys stay valid for back-compat and fold onto the ramp (normalmd, widelg, tighttight). The engine also emits --letter-spacing-normal (= md) and --letter-spacing-wide (= lg) as aliases so older consumers keep resolving.

Extension Tokens (Namespaced)

Theme-specific tokens use a --{theme-name}-* prefix. Components must never depend on these — they are consumed only by theme-specific CSS.

/* Space theme extensions */
--space-glass: color-mix(in srgb, var(--accent) 15%, rgba(0, 0, 0, 0.3));
--space-surface-elevated: color-mix(in srgb, var(--accent) 8%, #121220);

/* Veronica theme extensions */
--veronica-warmth: hsl(24, 80%, 55%);

5-Section Template

Every theme file follows this structure. Copy it as your starting point.

/* ── {Theme Name} — visor-core adapter ── */

/* ═══ Section 1: Shared tokens (mode-independent) ═══ */

.{theme-name}-theme {
  --accent: #5b6fff;
  /* --font-heading: "Custom Font", system-ui, sans-serif; */

  min-height: 100vh;
  background: var(--surface-page);
  color: var(--text-primary);
}

/* ═══ Section 2: Dark mode overrides ═══ */

.dark .{theme-name}-theme {
  --text-primary: /* dark value */;
  --surface-page: /* dark value */;
  --border-default: /* dark value */;
  --interactive-primary-bg: /* dark value */;
  /* ... all required tokens */
}

/* ═══ Section 3: Light mode overrides ═══ */

html:not(.dark) .{theme-name}-theme {
  --text-primary: /* light value */;
  --surface-page: /* light value */;
  --border-default: /* light value */;
  --interactive-primary-bg: /* light value */;
  /* ... all required tokens */
}

/* ═══ Section 4: Framework bridge ═══ */

/* fumadocs HSL triplets, if applicable */
.dark .{theme-name}-theme {
  --fd-background: /* H S% L% */;
  /* ... */
}

/* ═══ Section 5: Creative extensions (creative themes only) ═══ */

/* Namespaced extension tokens + custom CSS */

Theme Tiers

Standard Themes

Standard themes contain only token overrides — no custom CSS. They can be described entirely via .visor.yaml and are compatible with the theme builder (Phase 5).

name: "Corporate Blue"
tier: standard
tokens:
  shared:
    accent: "#1e40af"
  dark:
    text-primary: "#f3f4f6"
    surface-page: "#0a0a0f"
    # ... all required tokens
  light:
    text-primary: "#111827"
    surface-page: "#ffffff"
    # ... all required tokens

Use standard themes when: you need brand-aligned colors and typography without visual effects.

Creative Themes

Creative themes include section 5 — visual effects, animations, and custom CSS that go beyond token overrides. They still satisfy the full theme contract.

Use creative themes when: you want gradients, glass effects, starfields, custom animations, or any visual treatment that cannot be expressed as a token value.

The space theme is Visor's reference creative theme. It extends the contract with:

  • Starfield animation with twinkling stars
  • Glass/translucent code blocks
  • Sunrise gradient background (light mode)
  • Custom font faces (Satoshi, Source Code Pro)

Distinguishing rule: If a theme needs anything in section 5, it is creative. If sections 1-4 fully describe it, it is standard.

Always Both Modes

Rule: Every theme that sets colors.neutral must also set colors-dark.neutral.

The theme engine derives surface.muted and surface.elev in dark mode from the neutral role at shades 700 and 800. If colors.neutral is set in the light palette but no colors-dark.neutral override exists, the engine falls back to its default neutral (#6B7280 — Tailwind Gray 500), meaning every brand that skips the dark override produces identical dark mid-surfaces regardless of brand color.

This is a first-class authoring contract: any theme that defines a custom neutral must define its dark-mode counterpart.

In your theme YAML

colors:
  primary: "#6366f1"
  neutral: "#7c6f9b"   # brand-tinted neutral

colors-dark:
  neutral: "#3d3554"   # darkened + desaturated variant

A good dark neutral is typically the light neutral shifted toward lower lightness (L 20–35%) and reduced chroma — not pure gray. Start with 70% lower luminance and 60% of the original chroma as a baseline, then adjust for contrast.

Enforcing the rule in CI

Use --strict-dark with visor theme validate to promote the DARK_LIGHT_PARITY warning to a hard error:

npx @loworbitstudio/visor theme validate .visor.yaml --strict-dark

Migration path — opt-in to default:

  1. Add colors-dark.neutral to all themes that set colors.neutral.
  2. Verify locally: npx @loworbitstudio/visor theme validate .visor.yaml --strict-dark exits 0.
  3. Add --strict-dark to your CI validate step — once all themes pass, the flag is effectively the default.

Without --strict-dark, missing dark neutrals emit a DARK_LIGHT_PARITY warning (non-blocking). With it, they block CI until resolved.

Tips

  • Use color-mix() to derive surfaces from your accent color — this creates cohesive tinted surfaces.
  • Test both light and dark modes. A common mistake is only tuning one mode.
  • Keep --border-focus high-contrast against both light and dark backgrounds.
  • Reference the space theme source as a working example.