Color Theme Switcher — Free HTML CSS JS Snippet

Color Theme Switcher · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

6 curated accent themes — Indigo, Rose, Emerald, Amber, Sky, Violet — each with matched light and dark background tones specific to that hue
CSS custom properties on: root (--accent, --accent-dim, --bg, --surface, --text, --text2) updated via setProperty() for instant single-pass browser recalculation
Dark/light mode toggle with per-theme deep-tinted dark backgrounds rather than a generic dark gray, keeping color harmony in both modes
localStorage persistence — theme name and mode stored separately under cts-theme and cts-dark, applied before first paint to eliminate flash of default theme
Sliding settings panel with cubic-bezier(0.4,0,0.2,1) transform animation, translucent blur backdrop, and Escape-key close support
Live preview card inside the panel showing badge, primary button, and ghost button updating in real time as you change theme and mode
Swatch buttons with SVG checkmark active indicator and border ring; gear settings icon with 30-degree hover rotation as a visual affordance
Single applyTheme(themeName, isDark) function covers all 12 theme-and-mode combinations with no duplicated branching logic

About this UI Snippet

Color Theme Switcher — CSS Custom Properties, 6 Accent Themes, Dark Mode Toggle & localStorage Persistence

Screenshot of the Color Theme Switcher snippet rendered live

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:

text
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

  1. 1
    Open 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.
  2. 2
    Pick 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.
  3. 3
    Toggle 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.
  4. 4
    Watch 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.
  5. 5
    Reload 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.
  6. 6
    Add 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

SaaS dashboard user preferences panel
Ship this as the appearance section of a user settings page. Users pick an accent color that matches their workflow and toggle dark mode for late-night sessions. Persist the preference server-side with a fetch POST inside setTheme() so the choice survives across devices. Combine with a dashboard layout for a full settings page scaffold.
Design system theming demonstration
Drop this into a design system docs site to show every component reacting to a single CSS variable change in real time. It is the most effective way to explain token-based theming to engineers and stakeholders who have never seen CSS custom properties in action. Pair it with the theme palette generator to let visitors build and preview custom palettes.
Onboarding personalization step
Add a “Choose your color” step early in the onboarding flow to give users immediate ownership of the product. The localStorage save means the choice carries into the main app without any backend round-trip, and the live preview inside the settings panel lets users confidently commit to a theme before moving on.
Portfolio and personal site accent color picker
Offer visitors a color picker on your portfolio or personal site. The interaction is memorable, demonstrates CSS variable fluency, and makes the visit feel personal. Each accent subtly shifts the mood of the page — Violet reads as creative, Sky as minimal, Emerald as fresh — helping the portfolio stand out.
Teaching CSS custom properties and theming architecture
This snippet is a self-contained lesson in token-based theming. Students trace how setProperty() on document.documentElement propagates through --accent to every element, understand why isDark is a parameter to applyTheme() rather than a separate code path, and see localStorage persistence wired up with proper error handling for private browsing.
Component library accessibility color compliance
Centralize all accent colors in the THEMES object and audit each accent-on-surface pair for WCAG AA (4.5:1 contrast) in one place. When a color fails the contrast check, update it once in THEMES and every component that reads --accent is fixed instantly. No hunting through individual component stylesheets.

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.