More Navigation Snippets
Color Theme Switcher — Free HTML CSS JS Snippet
Color Theme Switcher · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Color Theme Switcher — CSS Custom Properties, 6 Accent Themes, Dark Mode Toggle & localStorage Persistence

When you visit a modern web app and it offers a dark mode or accent color picker, you immediately feel that the product respects your preferences. Users today expect theme customization for real reasons: high-contrast dark mode reduces eye strain during long sessions, accent color choices reinforce personal identity and brand trust, and accessibility requirements like WCAG demand sufficient contrast in every palette you ship. If your app only offers a single hard-coded color scheme, a growing segment of your users — especially those on OLED displays or with photosensitivity — will leave for a product that adapts to them. This color theme switcher HTML CSS JS snippet gives you a complete, production-ready starting point: 6 curated accent themes, a dark/light mode toggle, and a live preview panel, all driven by CSS custom properties and vanilla JavaScript with zero dependencies.
How CSS custom properties power the theming system
The entire system rests on six CSS variables declared on :root: --accent, --accent-dim, --bg, --surface, --text, and --text2. Every colored element — buttons, badges, borders, dots, code highlights — reads from these tokens rather than hardcoded hex values. When applyTheme() calls document.documentElement.style.setProperty('--accent', value) via setProperty(), the browser triggers a single recalculation pass that updates every element consuming that variable simultaneously. There is no class-toggling on hundreds of individual nodes, no stylesheet swapping, and no forced reflow. The cascade handles propagation instantly. You can pair this with a dark mode toggle for standalone mode switching or a color picker input to let users define fully custom accent values.
The 6 preset themes and the THEMES object
The THEMES object maps six named keys — Indigo, Rose, Emerald, Amber, Sky, and Violet — to a configuration record containing accent (the primary hex), accentDim (a transparent rgba tint used for hover backgrounds and chip fills), lightBg and darkBg (the page-level background for each mode), and lightSurface and darkSurface (the card/panel background). The dark variants are deep tinted backgrounds — for example, Indigo dark uses #0f1629 rather than a generic #1a1a1a — so each dark mode variant feels harmonious with its accent rather than a generic night mode. Adding a seventh theme is a single object addition; the buildSwatches() function iterates Object.entries(THEMES) and generates a swatch button automatically. Explore the theme palette generator and color swatch snippets to design and preview palettes before adding them here.
Dark/light mode toggle and the isDark flag
The isDark boolean is the second parameter of applyTheme(themeName, isDark). When setMode('dark') is called, it flips isDark to true and immediately re-applies the current theme using the dark background pair. The function then writes both values to localStorage independently — cts-theme stores the theme name and cts-dark stores '1' or '0'. Separating them lets users change theme and mode independently without either overwriting the other. Because loadPreference() runs before the first call to applyTheme(), the correct combination is applied before any elements paint, eliminating the white flash that plagues naive localStorage approaches. If no stored preference exists, you can fall back to prefers-color-scheme to honor the OS setting automatically (see the FAQ below).
The live preview panel — settings gear, swatches, and demo card
The theme settings panel slides in from the right edge using transform: translateX(100%) as its closed state and translateX(0) on the .open class. A cubic-bezier(0.4,0,0.2,1) easing curve — the same used by Material Design's standard transitions — makes the panel feel fast out of the gate and settle smoothly into place. Inside the panel, the swatch section renders six circular <button> elements colored with each theme's accent value. An SVG checkmark with opacity: 0 sits inside each swatch and becomes visible via the .active class when that theme is selected. Below the swatches, a mini preview card shows a badge, a primary button, and a ghost button all consuming the live CSS variables — so every change is visible before closing the panel. A gear icon button in the top-right of the main canvas opens the panel; it rotates 30 degrees on hover as a tactile affordance. You can see a similar full-page composition in the dashboard layout snippet.
Extending with custom themes and integrating into React or Next.js
To add a custom theme, append a key to the THEMES object with your desired accent, accentDim, lightBg, darkBg, lightSurface, and darkSurface values. The swatch and settings panel update automatically. In a React app, lift currentTheme and isDark into useState, call applyTheme() inside a useEffect that depends on both values, and place savePreference() in the same effect. For global access across routes, wrap the state in a React context so any component can read or update the theme without prop drilling. In Next.js specifically, use a suppressHydrationWarning attribute on <html> and apply the CSS variables via an inline <style> tag rendered server-side from a cookie value to eliminate the localStorage hydration flash entirely for authenticated sessions.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to trace through all twelve theme-and-mode combinations by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why applyTheme writes every value with root.style.setProperty instead of swapping a class on the body, and why loadPreference is called before the first applyTheme rather than after. The same assistant can help optimize it — for instance asking whether recomputing and reapplying all eight custom properties on every single mode toggle is wasteful when only the background and surface pair actually changes. It's also a good way to extend the system: ask it to add a seventh theme with matching light and dark surface tones, sync the choice to a user's account instead of just localStorage, or add a live prefers-color-scheme listener for first-time visitors who haven't set a preference yet. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a color theme switcher in plain HTML, CSS, and JavaScript that combines multiple named accent themes with an independent light/dark mode — no theming library, no frameworks.
Requirements:
- A THEMES object keyed by theme name, where each entry holds an accent color, a dimmed/tinted variant of that accent for hover backgrounds, and separate light-mode and dark-mode background and surface colors tuned to harmonize with that specific accent (not one generic dark gray shared by every theme).
- All themed elements (buttons, badges, borders, dots, card surfaces) must read exclusively from CSS custom properties declared on :root — never a hardcoded hex value in any component rule.
- A single applyTheme(themeName, isDark) function that is the only place allowed to call element.style.setProperty on the root element, updating every token (accent, accent-dim, background, surface, text colors, border, shadow) in one pass so the whole interface re-themes from one function call.
- Generate the theme swatch buttons dynamically by iterating the THEMES object's entries, rather than hardcoding a button per theme in markup, so adding a new theme requires only a new object entry.
- Persist the selected theme name and the dark/light flag to localStorage as two independent keys (not combined into one value), and read both back before the very first applyTheme call runs on page load so there is no flash of the default theme.
- A settings panel that slides in from the screen edge using a transform-based open/closed state (not display toggling, so the transition animates), containing the theme swatches, a light/dark mode toggle, and a small live preview card (badge, primary button, ghost button) that reflects every change immediately.
- Support closing the panel via a backdrop click, an explicit close button, and the Escape key.Step by step
How to Use
- 1Open the settings panel with the gear buttonClick the gear icon in the top-right corner of the demo. The settings panel slides in from the right with a smooth cubic-bezier animation and a translucent backdrop dims the content behind it. Close the panel at any time by clicking the backdrop, pressing Escape, or clicking the X button inside the panel header.
- 2Pick an accent color from the swatch circlesSix circular swatch buttons appear in the Accent Color section — Indigo, Rose, Emerald, Amber, Sky, and Violet. Click any swatch to apply that theme. A checkmark SVG appears on the active swatch and the entire demo updates instantly: the badge, buttons, feature dots, and borders all switch to the new accent color via CSS custom properties on document.documentElement.
- 3Toggle between light and dark modeClick Light or Dark in the Mode section. The applyTheme() function switches --bg and --surface to the theme's lightBg/darkBg or lightSurface/darkSurface pair. Dark variants use deep tinted backgrounds matched to each accent — Indigo dark is #0f1629, Emerald dark is #071a10 — so the dark mode feels color-coordinated rather than generic. Text, border, and shadow tokens also flip.
- 4Watch the live preview card update in real timeThe mini preview card at the bottom of the settings panel reflects every change you make before you close the panel. It contains a colored badge, a primary button, and a ghost button — all reading from the current CSS variables — so you can evaluate exactly how a theme looks on interactive elements before committing.
- 5Reload the page — your preferences are saved automaticallyEvery theme and mode change is written to localStorage under “cts-theme” and “cts-dark”. On the next page load, loadPreference() reads these values before applyTheme() runs, so the correct theme is applied before any elements paint. There is no flash of the default indigo theme. To reset to defaults, clear localStorage for the page origin.
- 6Add your own custom themes to the THEMES objectOpen the JS tab and add a new key to the THEMES object: provide accent (hex), accentDim (rgba with ~0.12 alpha), lightBg, darkBg, lightSurface, and darkSurface. The buildSwatches() function iterates Object.entries(THEMES) on init and generates a new swatch button automatically. No other code changes are required. For Next.js or React integration, lift the state into useState and call applyTheme() inside a useEffect to keep the DOM in sync.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
In loadPreference(), after reading localStorage, add a fallback for first-time visitors: if (localStorage.getItem(“cts-dark”) === null) { isDark = window.matchMedia(“(prefers-color-scheme: dark)”).matches; }. This reads the OS preference via prefers-color-scheme before the user has set their own choice. To respond to live OS changes (e.g. macOS auto-switching at sunset), add window.matchMedia(“(prefers-color-scheme: dark)”).addEventListener(“change”, e => { if (!localStorage.getItem(“cts-dark”)) { isDark = e.matches; applyTheme(currentTheme, isDark); } }). The guard ensures the OS listener does not override an explicit user preference stored in localStorage.
The localStorage approach works for unauthenticated users but causes a flash on hydration because React renders on the server before the client reads localStorage. To eliminate it: (1) write the theme to a cookie on every change instead of (or in addition to) localStorage; (2) read the cookie in your Next.js layout server component and inject an inline <style> tag into <head> with the resolved CSS variable values before any component renders; (3) add suppressHydrationWarning to the <html> element to silence the hydration mismatch warning that comes from server/client color differences. For authenticated users, store the preference in the database and read it during server-side rendering so the correct theme arrives with the initial HTML payload.
Open the JS panel and add a new entry to the THEMES object. The key becomes the theme identifier. Provide: accent (a hex color, e.g. “#e11d48”), accentDim (the same color as rgba with 0.12 alpha for hover tints), lightBg (the page background in light mode), darkBg (the page background in dark mode), lightSurface (card/panel background in light mode), darkSurface (card/panel background in dark mode), and label (display name shown in the demo badge). The buildSwatches() function calls Object.entries(THEMES) at init time, so your new swatch circle appears automatically without any other code changes. For React, update the THEMES constant file and the swatch map re-renders on the next render cycle.
Each accent color in this snippet is chosen so that white text on the accent background meets WCAG AA (4.5:1 contrast ratio for normal text, 3:1 for large text). To verify a custom accent, compute the relative luminance of both colors using the sRGB formula and apply (L1 + 0.05) / (L2 + 0.05). For dark mode surfaces, the deep tinted backgrounds (e.g. #1e2340 for Indigo dark) have luminance values low enough that the accent color still passes against them. Run every accent/surface combination through the WebAIM Contrast Checker or use the browser DevTools accessibility panel before shipping. If a custom accent fails on a light surface, increase its saturation or darken it slightly rather than changing the surface color, which would affect all themes.