docs

~/docs/customization

Scenes & colours

Make your own scene, change the accent, swap the font — without touching a component.

on this page

How a scene works

A scene is four inputs: a wallpaper, a base colour, an accent, and a dim strength. From those, src/styles/global.css mixes every other token with color-mix() — raised surfaces, the frosted nav capsule, glass panels, borders, text shades, the readability overlay and the edge scrims. Components only ever use the derived tokens, so a new scene is a config entry, not a stylesheet.

Scenes feed four tokens; everything else is mixed from them.
Scenes feed four tokens; everything else is mixed from them.
src/config/site.ts
scenes: {
default: 'matte',
options: [
{
id: 'matte', // stored in the visitor's localStorage — don't rename casually
label: 'matte', // shown in the menu
wallpaper: '/images/scenes/matte.webp', // or null for a flat colour
scheme: 'dark', // 'light' flips text, glass and border tokens
base: '#141416', // surface colour everything is mixed from
accent: '#a8a3f7', // links, prompts, focus rings, reading bar
dim: 0.32, // 0–1 overlay strength; busy wallpapers need more
},
],
},

The visitor’s choice is applied by a tiny inline script before first paint, so there is no flash of the wrong scene. Their wallpaper is preloaded; the others are warmed in the background after the page settles.

Making a wallpaper

Anything soft works: a blurred photo, a gradient, generated noise. Guidelines that held up:

  • 1920 × 1080 WebP, under 60 KB. It sits behind a grain overlay, so heavy compression is invisible. The five included scenes are 5–10 KB each.
  • Low contrast. Text is read on top of it. If a wallpaper is busy, raise dim rather than fighting it.
  • Match base to the wallpaper’s dominant colour. The overlay, scrims and capsule are mixed from base, and they read as part of the picture when the two agree.
Set `wallpaper: null` and the scene is just `base` with the grain overlay. A flat scene plus a single accent is the fastest way to a completely different look.

One scene, no switcher

Ship a single entry in scenes.options and the header menu, the t shortcut and the one-time hint all disappear. features.sceneSwitcher: false does the same with several scenes configured (the default is used for everyone).

Light scenes

scheme: 'light' swaps the ink: text shades are mixed from base toward black, glass and borders darken, and the nav capsule lifts toward white like paper. Pick a darker accent for light scenes — the included sand uses #5b51d8 against #a8a3f7 for the dark ones — so links keep their contrast.

Adjusting the derived tokens

If you want to change how surfaces are mixed rather than what they are mixed from, the knobs are at the top of src/styles/global.css:

src/styles/global.css
:root {
--lift-1: 6%; /* raised surfaces: how much white over base */
--capsule-lift-a: 14%; /* nav capsule top edge */
--glass-alpha: 5.5%; /* frosted panels */
--border-alpha: 9%;
--radius: 14px;
/* … */
}
[data-scheme='light'] {
/* the same knobs for light scenes */
}

Keep body text at 7:1 or better against base — the defaults are, and the overlay only makes the ground darker (or lighter) than base.

Fonts

Space Grotesk (variable, latin subset, ~22 KB) is self-hosted through Astro’s Fonts API and preloaded. It is configured in astro.config.ts:

astro.config.ts
fonts: [
{
provider: fontProviders.local(),
name: 'Space Grotesk',
cssVariable: '--font-space-grotesk',
fallbacks: ['ui-sans-serif', 'system-ui', 'sans-serif'],
options: {
variants: [{ src: ['./src/assets/fonts/space-grotesk-latin-wght-normal.woff2'], weight: '300 700' }],
},
},
],

To swap it, drop another .woff2 into src/assets/fonts/, change src and name, and update --font-sans in global.css if you change cssVariable. Any Fontsource package ships the files. The mono face is the visitor’s system monospace stack.

Code blocks

Expressive Code uses a single dark theme (github-dark-default) so snippets read as frosted panels in every scene, light ones included. To follow the scene instead, edit the integration in astro.config.ts:

astro.config.ts
expressiveCode({
themes: ['github-dark-default', 'github-light'],
themeCssSelector: (theme) =>
theme.type === 'dark' ? ':root:not([data-scheme="light"])' : '[data-scheme="light"]',
// …
}),

and drop the dark --code-bg override from the [data-scheme='light'] block in global.css.

Effects

Reading progress, back-to-top, the edge scrims, the header tuck and the reveal-on-scroll are each a flag in features. All of them are CSS scroll-driven animations except the header, which uses a ten-line scroll listener; browsers without scroll timelines simply show everything in place, and prefers-reduced-motion turns the reveals off.