Skip to content

Light/Dark Theme Toggle

Tailwind CSS dark mode toggle with an animated sun/moon switch that follows the system preference, remembers the choice and can reset to system.

Live preview

Open in new tab

Code

A light/dark theme toggle built with Tailwind CSS and vanilla JavaScript. It starts from the visitor's operating system preference, lets them override it with an animated sun/moon switch, remembers the choice in localStorage, and offers a one-click way back to the system setting. A small preview card shows the theme change live, which makes it a good fit for settings pages, dashboards and blog headers.

How it works

The root .ttg-root defines all colors as CSS custom properties in style.css, and the markup uses them through arbitrary-value utilities such as bg-[var(--ttg-bg)] and text-[var(--ttg-muted)]. Adding the ttg-dark class to the root swaps the whole token set, so every element inside changes at once.

The switch is a button with role="switch" marked data-ttg-switch. Its .ttg-knob slides with transform when aria-checked="true", while .ttg-knob-sun and .ttg-knob-moon cross-fade and rotate. On DOMContentLoaded the script reads the key named in data-ttg-storage; if nothing is stored it uses matchMedia('(prefers-color-scheme: dark)'). Clicking the switch stores dark or light, and the data-ttg-reset button removes the stored value so the component follows the system again (it is disabled while nothing is stored). The script also listens to the media query change event, to the storage event for other tabs, and to a ttg:sync event so several toggles on one page stay in step. Every change fires a bubbling ttg:change event with detail.theme. The ttg-ready class turns on transitions after the first paint so the page does not animate on load. If storage is blocked, the choice is kept in memory for the session.

Customization

  • data-ttg-scope on .ttg-root: set it to document to also toggle a dark class and color-scheme on <html>, so the rest of your site can react to the theme.
  • data-ttg-storage: change the localStorage key, for example to share one preference across several apps on the same domain.
  • --ttg-accent and --ttg-on-accent in both the .ttg-root and .ttg-root.ttg-dark blocks: the violet accent of the knob icon, avatar and primary button.
  • --ttg-bg, --ttg-surface, --ttg-text and --ttg-border: page, card, text and border colors for each theme.
  • --ttg-speed: duration of the color fade and knob slide.
  • translateX(2rem) on .ttg-switch[aria-checked="true"] .ttg-knob: adjust it if you resize the w-[4.5rem] track.
  • Listen for ttg:change on the root to sync the theme with a charting library or a server-side preference.

Accessibility

The control is a native button with role="switch" and aria-checked, labelled by the visible "Dark mode" text and described by the status line, so screen readers announce "Dark mode, switch, on". Space and Enter toggle it. The status message (data-ttg-status) is an aria-live region that explains whether the theme is saved or follows the system. Icons are aria-hidden. Both themes meet WCAG AA text contrast, focus rings use the accent color, and all transitions are disabled under prefers-reduced-motion: reduce.

Usage notes

To avoid a flash of the wrong theme on full sites, also add a tiny inline script in your <head> that reads the same storage key before the page renders.

Requirements

Add these to your page (already included in the download):

  • <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

The Tailwind browser build is for prototyping. In production, compile Tailwind with its CLI or your bundler.

Light/Dark Theme Toggle – Tailwind CSS theme toggles component screenshot
Light/Dark Theme Toggle · Tailwind CSS Theme Toggles