C CoolAdmin v3.3.0

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:

SwatchIDHex
Blue (default)blue#4272d7
Purplepurple#7c3aed
Tealteal#0d9488
Roserose#e11d48
Amberamber#d97706
Graphitegraphite#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-accent cascade doesn’t exist. Switching presets would do nothing visible.
  • Auth and error pageslogin.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-coralid: '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