Theme switcher
A floating palette button in the bottom-right corner of every page opens a panel with six color presets — Blue, Purple, Teal, Rose, Amber, Graphite. Click a swatch to retone the whole UI. Preference persists in localStorage.
Last updated May 22, 2026
The theme switcher is a small floating widget that drives CoolAdmin’s six built-in color presets. It mounts automatically on every page where the modern overlay (body.app) is active, builds its UI from a config array, and persists the chosen preset to localStorage.
Implementation: initThemeSwitcher() in js/main-vanilla.js.
What it looks like
A palette icon button anchored to the bottom-right of every dashboard page. Click it — a panel slides out with six color swatches:
| Swatch | ID | Hex |
|---|---|---|
| Blue (default) | blue | #4272d7 |
| Purple | purple | #7c3aed |
| Teal | teal | #0d9488 |
| Rose | rose | #e11d48 |
| Amber | amber | #d97706 |
| Graphite | graphite | #334155 |
Click a swatch to apply. A toast confirms (“Switched to Purple”). The chosen preset persists across pages and survives reloads.
How the apply works
The switcher’s apply function is just a class swap on <body>:
function applyTheme(id) {
body.classList.remove(...THEMES.map((t) => `theme-${t.id}`));
body.classList.add(`theme-${id}`);
}
The actual color cascade lives in src/scss/app/_theme-presets.scss, where each preset class redefines the --m-accent token group:
body.app.theme-purple {
--m-accent: #7c3aed;
--m-accent-rgb: 124, 58, 237;
--m-accent-hover: #6b2fd0;
--m-accent-soft: #ede9fe;
}
Every UI element that reads var(--m-accent) — buttons, links, focus rings, badges, chart strokes — retones automatically. No JS-driven inline-style mutations, no element-level updates.
Persistence
const STORAGE_KEY = 'cooladmin.theme';
try {
return localStorage.getItem(STORAGE_KEY);
} catch (_) { return null; }
The chosen id is written to localStorage.cooladmin.theme. On the next page load, initThemeSwitcher() reads the value and applies the matching theme before the user sees any UI — so navigating between pages keeps the preset stable.
try/catch wraps both reads and writes so private-mode browsers (where localStorage throws) gracefully fall back to the default Blue.
Where it doesn’t mount
The switcher only renders when both conditions are met:
if (!body.classList.contains('app')) return;
if (body.classList.contains('auth-page') || body.classList.contains('error-page')) return;
- Pages without
body.app— the modern overlay isn’t loaded, so the--m-accentcascade doesn’t exist. Switching presets would do nothing visible. - Auth and error pages —
login.html,register.html,forget-pass.html,404.html,500.html,maintenance.html. These have full-screen centered layouts; a floating widget in the corner would crowd the design.
If you want the switcher on a custom page, ensure <body class="app"> is set and the page doesn’t carry auth-page or error-page.
Adding a new preset
Three steps.
1. Add the CSS
Append a new block to src/scss/app/_theme-presets.scss:
body.app.theme-coral {
--m-accent: #ff6b35;
--m-accent-rgb: 255, 107, 53;
--m-accent-hover: #e85a28;
--m-accent-soft: #fff1e9;
}
Three derived values per preset:
--m-accent— the base color--m-accent-hover— usually ~15% darker, used on:hover/:focus--m-accent-soft— a heavily tinted version for badge / chip backgrounds
The --m-accent-rgb triplet exists for rgba(var(--m-accent-rgb), 0.2) patterns where you need to set opacity at the use site.
2. Add the swatch
Append to the THEMES array in initThemeSwitcher():
const THEMES = [
{ id: 'blue', label: 'Blue', color: '#4272d7' },
{ id: 'purple', label: 'Purple', color: '#7c3aed' },
{ id: 'teal', label: 'Teal', color: '#0d9488' },
{ id: 'rose', label: 'Rose', color: '#e11d48' },
{ id: 'amber', label: 'Amber', color: '#d97706' },
{ id: 'graphite', label: 'Graphite', color: '#334155' },
{ id: 'coral', label: 'Coral', color: '#ff6b35' }, // ← new
];
The id must match the SCSS class suffix (theme-coral ↔ id: 'coral').
3. Rebuild
npm run build # sass compile + pug compile
The next page load picks up the new swatch in the panel. Existing users with a stored preset stay on their saved value; the new preset is just an additional option.
Setting the theme programmatically
If you want to apply a preset from elsewhere — say a settings page — the simplest path is to read/write the class + localStorage manually:
function setTheme(id) {
document.body.classList.remove('theme-blue', 'theme-purple', 'theme-teal',
'theme-rose', 'theme-amber', 'theme-graphite');
document.body.classList.add(`theme-${id}`);
try { localStorage.setItem('cooladmin.theme', id); } catch (_) {}
}
The switcher widget will pick up the new value on the next page navigation (it reads from localStorage on init).
Why a floating button, not a sidebar item or keyboard shortcut
The earlier docs draft claimed Cmd/Ctrl+Shift+T opens the switcher — that was incorrect. There’s no keyboard binding in initThemeSwitcher().
Reasons the floating-button approach was chosen:
- Discoverable. The palette icon is visible in the corner of every page; users don’t need to learn a shortcut to find it.
- Doesn’t crowd the sidebar. The sidebar is already dense with nav items. Adding a “Themes” entry would compete for space with actual product pages.
- Stateful affordance. The button stays visible after the panel closes, signaling that the panel is reachable again.
If you want a keyboard shortcut, the simplest patch is to add one in initThemeSwitcher() right after the toggle.addEventListener('click', …) block:
document.addEventListener('keydown', (e) => {
if (e.shiftKey && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 't') {
e.preventDefault();
wrap.classList.toggle('is-open');
}
});
Toast confirmation
Switching presets fires a success toast via the global toast system:
if (window.toast) window.toast.success(`Switched to ${t.label}`);
The toast system is initialized alongside the switcher in main-vanilla.js. See initToastSystem() (line 948-ish) for the full API — window.toast.success(message), .info, .warning, .error.
See also
- Theming — the token system the switcher operates on
- ⌘K command palette — the other runtime UI feature
- Architecture — where
main-vanilla.jsruns in the script load order