Deterministic
The same ThemeState always emits byte-identical CSS, so exported stylesheets diff cleanly in version control.
This page exists to be themed, not to be read start to finish. It deliberately packs in every content surface Starlight renders — headings, lists, tables, asides, code blocks, and the built-in components — so that a single accent hue change or treatment swap can be checked against the whole site in one place.
Starlight Customizer supports bold text, italic text, and inline code in the same sentence, plus links to other pages and links to external sites. None of that is unusual for a docs site, but a customizer needs to get every one of those right at once, because a real page mixes them constantly.
Here is a paragraph long enough to wrap across several lines at a typical content width, which is exactly the kind of text a line-height or body-font-size control needs to be checked against. Starlight Customizer’s typography controls change the body font, the heading font, the monospace font, the base size, the type scale ratio, and the line height independently, and a short sentence or two rarely reveals whether a scale ratio has gone too aggressive — you need a paragraph like this one, with enough words per line to show where a font’s default line-height starts to feel cramped or airy once the base size changes underneath it.
An unordered list, with a nested sub-list:
An ordered list:
theme.css and APPLY-THEME.md.A loose list, where each item is its own paragraph:
The panel persists its state to localStorage on every change, since Starlight does a full page load between routes rather than a client-side transition.
The same state is also encodable into location.hash on demand, behind a “Copy share link” button, so a colleague can open an in-progress theme without you sending a file.
Import accepts either a pasted state.json or a pasted sidebar config, so migrating from a hand-written astro.config.mjs doesn’t require starting over.
ThemeState into a CSS string. It never touches the DOM or localStorage.A theme editor that can’t show you the real page is just a color picker with extra steps.
Starlight’s table of contents in this fixture is configured for levels 2 through 4, so this heading and the one below it won’t appear in the “On this page” panel — but they still exist in the DOM, which matters for a customizer checking heading-level CSS in isolation from TOC filtering.
The smallest heading level Markdown supports. Real docs rarely go this deep, but the customizer’s typography scale still needs a value for it.

Press Cmd + K on macOS, or Ctrl + K on Windows and Linux, to jump straight to the panel’s control search box.
It stays. The customizer writes into a single stylesheet loaded via Starlight’s customCss option; anything else you’ve added under src/styles/ continues to load exactly as before, and cascade layer ordering keeps your own rules able to override the generated ones if you set them up that way.
| Control ID | Group | Type | Default | Min | Max | Step | Tier | Notes |
|---|---|---|---|---|---|---|---|---|
color.accent.hue |
Colors | range | 250 |
0 |
360 |
1 |
1 |
Degrees on the OKLCH hue wheel |
color.accent.chroma |
Colors | range | 0.12 |
0 |
0.4 |
0.01 |
1 |
Higher values look more saturated |
type.font.body |
Typography | font | inter |
— | — | — | 1 |
A Fontsource variable font id |
layout.contentWidth |
Layout | range | 48 |
32 |
80 |
1 |
1.5 |
In rem |
sidebar.activeStyle |
Sidebar | select | filled-pill |
— | — | — | 2 |
One of 4 options |
toc.currentStyle |
TOC | select | default |
— | — | — | 2 |
One of 4 options |
code.theme |
Code | select | starlight-default |
— | — | — | build |
Approximated in preview only |
page.toc.minLevel |
Page options | range | 2 |
1 |
6 |
1 |
build |
Approximated via DOM filtering |
page.pagination |
Page options | toggle | true |
— | — | — | build |
Hides .pagination-links when off |
export function emitCss(state, { forPreview = false } = {}) { const lines = [headerComment()]; if (!forPreview) lines.push(...fontImports(state)); lines.push(...darkTokens(state), ...lightTokens(state)); return lines.join('\n');}npm install @fontsource-variable/intercp theme.css src/styles/theme.cssgit add src/styles/theme.cssgit commit -m "Apply Starlight Customizer theme"import { presets } from './presets.js';import { setValue } from './state.js';import { emitCss } from './emit-css.js';
export function applyPreset(state, presetId) { const preset = presets.find((p) => p.id === presetId); if (!preset) throw new Error(`Unknown preset: ${presetId}`); return { ...state, values: { ...preset.values } }; return { ...state, preset: presetId, values: { ...preset.values } };}import { emitCss } from './customizer/core/emit-css.js';import { defaultState, setValue } from './customizer/core/state.js';
const state = setValue(defaultState(), 'color.accent.hue', 260);const css = emitCss(state);# The emitter itself is JS-only; call it through Node from a Python build step.import subprocess
css = subprocess.run( ["node", "scripts/emit-theme.mjs", "state.json"], capture_output=True, text=True, check=True,).stdoutnode scripts/emit-theme.mjs state.json > src/styles/theme.cssUse Cmd + K to open the panel’s search box and jump straight to a control by name.
Use Ctrl + K to open the panel’s search box and jump straight to a control by name.
Use Ctrl + K (or Super + K on some window managers) to open the panel’s search box.
theme.css into src/styles/theme.css in your real project.APPLY-THEME.md: install any fonts, add customCss to your Starlight config, and redeploy.Deterministic
The same ThemeState always emits byte-identical CSS, so exported stylesheets diff cleanly in version control.
Framework-free
No React, Vue, or Svelte — the panel is a single web component built from plain ES modules.
Reversible
Every control has a per-control reset, and the whole panel has a “Reset all” that returns to the active preset.
Default: Default Default Default
Note: Note Note Note
Tip: Tip Tip Tip
Caution: Caution Caution Caution
Danger: Danger Danger Danger
Success: Success Success Success