Skip to content
sdocs

Theming

sdocs has four independent theming systems:

  1. Explorer theme (light/dark) — controls the Explorer UI itself
  2. Customization axes — named dimensions of your design system the reader can switch: theme, density, palette
  3. A scale slider — the one dimension that is a range rather than a set of names
  4. Preview stylesheets — which CSS file is loaded into the preview iframe

Explorer theme (light/dark)

The sdocs UI has a light/dark toggle in the top bar. The choice is persisted to localStorage under sdocs-theme and applied by setting data-sdocs-theme="light" or "dark" on the app's own root element (the .sdocs-app container, not <html>).

If you're styling the app shell, target these attributes:

[data-sdocs-theme="light"] { /* … */ }
[data-sdocs-theme="dark"]  { /* … */ }

Customization axes

A design system usually varies along more than one dimension at once — light and dark, compact and airy, one brand palette or another. Declare each as an axis and sdocs puts a control in the top bar for it:

// sdocs.config.js
axes: [
  { id: 'scheme',  label: 'Theme',   values: ['light', 'dark'] },
  { id: 'density', label: 'Density', values: ['airy', 'compact'] },
  { id: 'palette', label: 'Color',   values: ['blue', 'red', 'olive'] },
]

The reader's pick lands on every preview, example, and layout as a data- attribute on the stage document's <html>:

<html data-scheme="dark" data-density="compact" data-palette="olive">

sdocs never interprets an axis. It renders the control and writes the attribute; your CSS supplies the meaning. That's what lets a project declare any axes it likes without sdocs knowing the vocabulary:

[data-density="compact"] { --space-md: 8px; --control-h: 32px; }
[data-palette="olive"]   { --color-accent: oklch(0.62 0.11 125); }

The first value listed is the default. label is optional and falls back to the capitalized id. Ids are lowercase letters, digits and dashes; the sdocs- prefix is reserved for the attributes that carry a stage's own identity.

How the controls render

Each axis is a compact segmented control while the bar has room for it, and every axis collapses to a dropdown together once they'd start pushing the section tabs out of view. On a narrow viewport they move into the navigation drawer, where there's space for visible labels.

Nothing to configure — it's measured. Declaring more axes, or axes with longer values, just means the switch happens at a wider window.

Dark mode needs the attribute

A dark theme written only as a media query can't be switched — prefers-color-scheme is the browser's setting, not something a dropdown can override. Make the attribute the source of truth, and set color-scheme alongside it so native controls, scrollbars and form widgets follow:

[data-scheme="dark"] {
  color-scheme: dark;
  --color-bg: #0f1115;
  --color-text: #e8eaed;
}

Where the picks are kept

Selections persist to localStorage under sdocs-axes and are validated against the config on load: if you rename a value, a reader carrying the old one falls back to the default rather than getting an attribute no stylesheet matches.

Addressing one combination

A stage page accepts its axes as URL parameters, so a single preview can be opened — or screenshotted — in an exact combination without clicking through the Explorer:

/preview/button/sizes?axis-scheme=dark&axis-density=compact

A continuous knob

Some dimensions aren't a set of names. Declare a scale and the top bar gets a slider beside the axis controls:

// sdocs.config.js
scale: { min: 0.75, max: 1.5, default: 1, step: 0.05 }

Its value lands on every stage's root as a CSS custom property rather than an attribute:

<html style="--scale: 1.25">

which is what your css multiplies by:

.card { padding: calc(8px * var(--scale, 1)); }

The property is the point of the difference. An axis names its values, so [data-density="compact"] can carry a whole block of rules; a range can't, and data-scale="1.25" would need a rule per step. One rule reading a number covers the whole range.

var renames the property (--ui-scale), label renames the control. Clicking the control's label or its value — anything but the slider itself — returns it to the default, as does double-clicking the slider. The pick persists like the axes, and a stage page accepts ?scale=1.25 — plus ?scale-var=--ui-scale when the project renamed it, since a stage opened on its own has no parent to ask.

The label drops away when the bar runs short of room, at the same width the axis switches give up their names for dropdowns: by then the row needs the space more than the reader needs the word, and the value stays either way.

Named stops

Add presets and the common sizes get buttons, sitting with the slider in the same control — to the right of its value:

scale: {
  min: 0.75, max: 1.5, default: 1, step: 0.05,
  presets: [
    { label: 'S', value: 0.875 },
    { label: 'M', value: 1 },
    { label: 'L', value: 1.25 },
  ],
}

Picking one sets the slider, and the slider still reaches everything between — move it off a stop and no preset shows as active. A preset outside minmax is refused with a warning rather than clamped: a button labelled XL that quietly lands on something else is worse than one that isn't there.

Always give the var() a fallback. A stage is not the only place that css runs, and calc(8px * var(--scale)) collapses to nothing wherever the property is unset.

Preview stylesheets

Stylesheets loaded into the preview iframe are controlled by the css option in sdocs.config.js.

Single stylesheet

css: './src/styles/global.css'

One stylesheet, always loaded.

Named stylesheets

css: {
  light: './src/styles/light.css',
  dark: './src/styles/dark.css',
  highContrast: './src/styles/high-contrast.css',
}

With more than one named stylesheet, sdocs shows a dropdown in the top bar letting users switch between them. Only one is active at a time — switching disables the others.

Reach for axes instead when the variants multiply: three palettes × two densities is six stylesheets to maintain but two axes to declare, and axes compose where whole-file swaps can't.

This is useful for:

  • Light/dark variants of your design system
  • Brand variants (Acme vs. AcmeKids)
  • Accessibility variants (high contrast)

Explorer theme vs. preview theme

These are separate — the sdocs UI being in dark mode doesn't automatically switch the preview to a dark stylesheet, or flip a scheme axis. Users control each independently, and the Explorer's own chrome keeps its appearance no matter what your design system is doing. That's deliberate: a design system mid-refactor shouldn't be able to make the docs unreadable.

If you want them to track each other, name your stylesheets light and dark — the sdocs app theme names match, so mentally they'll line up even though the switching is manual.

Path resolution

  • Relative paths (./styles/…) — resolved against the project root
  • Absolute paths (/styles/…) — used as-is
  • HTTP(S) URLs — passed through unchanged

See also