Manage theme palettes
Work with the theme manager in a scaffolded Aurora frontend: switch the active color palette from the header, add a brand-new palette from a tweakcn.com export in a single command (CSS, registration, and font wiring included), and understand why the selector disappears in a production build.
For the why behind it — the three-layer resolution chain, the anti-FOUC flow, and how palettes propagate to the whole UI including fonts and charts — see the concept Theme palettes. FOUC (Flash Of Unstyled — or Incorrect — Content) is the flicker where, on load, you briefly see the “wrong” look before the correct styling kicks in.
Two terms used throughout this guide:
- Palette — the color theme (
theme-neutral,theme-amber-minimal, …), managed by the framework serviceThemePaletteService(imported from@aurora). This is the “theme” you add and switch. - Appearance — the orthogonal light / dark / system mode. A separate selector; changing one never touches the other.
The active palette resolves localStorage["theme-palette"] → environment.appearance.theme → 'theme-neutral' (first match wins).
Switch palettes from the header
Section titled “Switch palettes from the header”The palette selector lives in the header (frontend/src/app/domains/admin/layout/site-header.ts) behind the swatch-book icon. It iterates service.palettes (the list from environment.appearance.palettes), marks the active one with a check, and on click calls ThemePaletteService.set(id) — which swaps the class, loads the palette’s font if needed, persists the choice, and updates the signal. It is completely independent of the light/dark selector sitting next to it.
Add a new palette
Section titled “Add a new palette”Adding a palette is one command. It writes the CSS, registers the palette in every environment, and wires up its web font — no manual editing of the service or the env files.
-
Design and export the palette at tweakcn.com and save the raw CSS under
themes/(e.g.themes/theme-ocean.css). -
Run the importer from the project root (where the
pnpmscripts and thethemes/folder live) with--append:Terminal window pnpm theme:adapt themes/theme-ocean.css theme-ocean --name "Ocean" --appendThat single command:
- appends the two scoped CSS blocks (
:root .theme-oceanand:root.dark .theme-ocean) tofrontend/src/styles.css; - registers
{ id, label, fontHref? }in all fiveenvironment*.tsfiles — idempotent, so re-running never duplicates an existing id; - detects the web font from the export (
--font-sans/--font-serif/--font-mono, skipping system stacks) and stores the Google Fonts URL asfontHref.
--nameis optional; without it the label is derived from the id (theme-amber-minimal→ “Amber Minimal”). Drop--appendto print the adapted CSS to stdout without touching any file.The resulting registry entry (in each env) looks like:
// frontend/src/environments/environment.ts (and the other four envs)appearance: {theme: 'theme-neutral', // build default (must be one of the palettes below)layout: '',themeSelector: true, // show the header selector in this buildpalettes: [{ id: 'theme-neutral', label: 'Neutral' },{id: 'theme-ocean',label: 'Ocean',fontHref: 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap',},],}, - appends the two scoped CSS blocks (
-
Reload the app. The palette appears in the header selector (on builds with
themeSelector: true), and its font loads itself if it declared one.
That is the whole flow: CSS + registration across the five envs + font detection, in one command.
How the importer transforms the export
Section titled “How the importer transforms the export”pnpm theme:adapt is implemented in scripts/theme/adapt-tweakcn.ts by pure functions — adaptTweakcnExport, detectFontHref, registerPaletteInEnvSource, deriveLabelFromId (tests: pnpm test:theme). Signature:
pnpm theme:adapt <export.css> <theme-id> [-n|--name "Label"] [--append]Given a raw tweakcn export (:root {}, .dark {}, @import, @theme inline, …) it:
- Emits
:root .theme-<id>from the export’s:rootblock and:root.dark .theme-<id>from its.darkblock, injecting the matchingcolor-scheme. - Strips
@import,@custom-variant,@layer base, and@theme inline— those are supplied globally by the Spartan preset, so a per-palette copy would be redundant or conflicting. - Detects the web font (first family of each
--font-*, system stacks filtered out) and builds thefontHref. - Warns if the export had no
.darkblock (only the light block is emitted).
With --append it writes the CSS to frontend/src/styles.css and registers the palette across the five envs; without it, it just prints the CSS to stdout.
Fonts follow the theme
Section titled “Fonts follow the theme”You normally don’t touch fonts by hand — the importer detects them and the framework loads them. Two pieces make that work, both covered in depth in the concept:
- Applied:
frontend/src/styles.cssbindsfont-family: var(--font-sans, …)on<body>, so typography follows the active palette’s token. - Loaded: when a palette declares a
fontHref,ThemePaletteServiceinjects its<link rel="stylesheet">into<head>on demand — once, and only for the active palette. System-stack palettes carry nofontHrefand load nothing.
See Theme palettes › How fonts follow the theme for the mechanism and the login-page edge case.
Hide the selector in production
Section titled “Hide the selector in production”The selector is gated by a build-time flag, environment.appearance.themeSelector: boolean. The header renders the button inside @if (showPaletteSelector), where showPaletteSelector = environment.appearance?.themeSelector ?? false.
| Environment file | themeSelector |
|---|---|
environment.ts (base) | true |
environment.dev.ts | true |
environment.local.ts | true |
environment.qa.ts | true |
environment.prod.ts | false |
A production build therefore ships without the selector; dev / local / qa builds show it. Hiding the button does not change the active palette — environment.appearance.theme plus localStorage still decide what the app renders.
Notes for developers
Section titled “Notes for developers”- Color is always a theme token, never hardcoded. Categorical series →
--chart-1..5; semantic states (success / error) → semantic tokens such as--destructive. A hardcoded hex will not follow palette or light/dark changes. - Catalog charts handle theming for you.
<aurora-chart>(@aurora/components/chart) already reads--chart-*and re-renders on every palette or light/dark change — you do not need to do anything special. See Theme palettes › How charts stay in sync for the mechanism. - Anticipated, not yet implemented: per-tenant runtime theming — calling
service.set(tenantTheme)from theprovideAuthenticatedInitializerhook once the session is established. Build the chain assuming this may arrive; nothing in it needs changing today.
Related
Section titled “Related”- Theme palettes — the concept: resolution model, anti-FOUC flow, fonts, propagation.
- tweakcn.com — the visual theme editor the importer consumes.