Skip to content

Flavors & theming

A flavor is a complete palette: neutrals from the deepest background to body text, plus twelve accents. Static Bloom has four, one night in the city from dusk to dawn.

Flavor Theme Character Use for
dusk dark Plum ink, the lightest dark base Long reading
midnight dark Saturated midnight indigo Websites and docs, default
blackout dark Near-black steel, highest contrast In-game HUDs
daybreak light Pale dawn lavender, deeper accents Light mode

The three dark flavors share the exact same accent values; only their neutrals change.

preference auto · resolved midnight · theme dark

This is the Vue SbFlavorSelect from @static-bloom/vue. It shares one runtime with the flavor select in the header: change either and the other follows, and the choice survives a reload.

Attribute Values Effect
data-flavor dusk, midnight, blackout, daybreak Selects the palette for the element and its descendants
data-theme dark, light Set with the flavor; drives the Tailwind dark: and light: variants and Starlight
data-accent one of the 12 accents Re-points --sb-primary, and with it links, focus rings and glow
<html data-flavor="midnight" data-theme="dark" data-accent="plasma"></html>

With no data-flavor on the root, Midnight applies, or Daybreak when the system prefers a light color scheme. An explicit attribute always wins over that fallback.

Every semantic token is re-declared on each [data-flavor] and [data-accent] element, so a nested scope resolves against its own palette:

<section data-flavor="daybreak" data-theme="light">
<button class="sb-btn">Daybreak</button>
<div data-flavor="blackout" data-theme="dark" data-accent="acid">
<button class="sb-btn">Blackout, acid accent</button>
</div>
</section>
online
online

data-accent swaps the primary intent for one subtree. Everything derived from it follows: solid buttons, links, focus rings and glow.

An inner data-accent beats an outer one, and an accent set on an ancestor carries into nested flavor scopes. Avoid nesting more than two accent scopes; past that, CSS cannot pick the nearest one.

The visitor’s preference is stored in localStorage under static-bloom:flavor as auto, dusk, midnight, blackout or daybreak. auto follows prefers-color-scheme: a light system gets the configured light flavor (Daybreak), anything else the configured dark flavor. Every package reads and writes the same key, so a choice made on the docs carries over to an Astro site or a Vue app on the same origin.

The Astro integration, the Astro FlavorScript component and the Starlight plugin all install the same runtime as window.staticBloom. It sets data-flavor, data-theme, data-accent and color-scheme on <html>:

window.staticBloom.getFlavor() // stored preference, including 'auto'
window.staticBloom.resolve() // concrete flavor in use, e.g. 'midnight'
window.staticBloom.setFlavor('dusk') // persists, applies, dispatches the event
document.addEventListener('static-bloom:flavor-change', ({ detail }) => {
console.log(detail.flavor, detail.resolved, detail.theme) // 'dusk' 'dusk' 'dark'
})

static-bloom:flavor-change fires on initialization, once the DOM is ready, on every setFlavor call and, while the preference is auto, when the system color scheme changes. setFlavor throws a TypeError for unknown values. When storage is blocked, the choice still applies for the current page.

<script setup lang="ts">
import { useFlavor } from '@static-bloom/vue'
const { flavor, resolvedFlavor, isDark, flavors, setFlavor } = useFlavor()
</script>
<template>
<button v-for="item in flavors" :key="item.id" @click="setFlavor(item.id)">
{{ item.label }}
</button>
</template>

Every call inside one Vue app shares a single state. On a page with window.staticBloom, useFlavor delegates to the runtime and mirrors its events, which is how the island above stays in sync with the header. Without a runtime (an in-game page), it runs standalone: it persists under the same key, follows the system preference while auto, and sets data-flavor, data-theme and color-scheme on <html> or on a target element. The first call can pass options:

useFlavor({
defaults: { dark: 'blackout', light: 'daybreak' }, // flavors used by 'auto'
defaultFlavor: 'auto', // when nothing is stored
storageKey: 'static-bloom:flavor',
target: () => document.querySelector<HTMLElement>('#hud'), // null applies nothing
})

It is SSR-safe: nothing touches window or document until a component mounts.

The runtime is a small inline script in <head> that runs before the first paint, so the stored flavor is already on <html> when the page draws. The Astro integration injects it into every page; on standalone Astro pages, put FlavorScript in the head slot of Layout. Without JavaScript, the CSS fallback still applies: Midnight, or Daybreak for a light system preference.