BESPA bakes Material Design 3 into the framework: every built-in widget references the design tokens (--md-sys-color-*, --md-sys-typescale-*) so a page automatically picks up your theme. This page is about the app-level controls — picking dark vs. light, choosing a key color palette, and persisting the user’s preference.
Three methods on Page set the appearance mode:
// Always render dark.
page := wf.Page().WithThemeDark()
// Always render light.
page := wf.Page().WithThemeLight()
// Follow the OS / browser preference (the default).
page := wf.Page().WithThemeDefault()
The default follows the browser’s prefers-color-scheme media query, so the page tracks the OS setting and even switches live if the user toggles the system theme. WithThemeDark / WithThemeLight force one or the other regardless of the OS.
Material 3 generates an entire token palette — primary / secondary / tertiary / error / surface, with their on-color counterparts — from a single “source” color. The css package exposes presets plus a constructor that derives a palette from any color string:
// Pick from the framework's preset palettes.
page := wf.Page().WithKeyColors(css.PresetKeyColors[0])
// Or build your own from a single source color.
custom := css.KeyColorsFromString("violet")
page := wf.Page().WithKeyColors(custom)
All built-in widgets and any widget that references --md-sys-color-* recolors automatically; no widget needs to know which palette is in use.
User-specific preferences belong in the session, not the URL. The website’s shared.Render wrapper reads the active session’s saved theme/palette and applies them to every page before drawing:
func Render(w http.ResponseWriter, r *http.Request, page *widget.PageWidget) {
session := SessionOf(w, r)
switch session.Theme {
case "Dark":
page.WithThemeDark()
case "Light":
page.WithThemeLight()
}
if session.Palette != "" {
for _, kc := range css.PresetKeyColors {
if kc.Name == session.Palette {
page.WithKeyColors(kc)
break
}
}
}
page.Draw(w, r)
}
For a real app, swap the in-memory session for whatever persistence you use; the page-side API stays the same.
On the first request the framework writes the resolved palette and typography scale to /bespa/tones.css and /bespa/style.css. Browsers cache those CSS files; a 24-hour cache header keeps revisits cheap. When the user switches themes, the new key colors are picked up on the next page navigation — there’s no client-side palette computation.
Extend → Material theming — how custom widgets should reference tokens so they recolor for free.
Profile page — a working theme/palette switcher backed by session storage.