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.
Try it
Section titled “Try it”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.
Three attributes
Section titled “Three attributes”| 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.
Nested scopes
Section titled “Nested scopes”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>Accents
Section titled “Accents”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.
Persisting the choice
Section titled “Persisting the choice”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 page runtime
Section titled “The page runtime”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.
Vue: useFlavor
Section titled “Vue: useFlavor”<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.
No flash of the wrong flavor
Section titled “No flash of the wrong flavor”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.