Skip to content

System, Light and Dark Theme Dropdown

Accessible Tailwind CSS dropdown to choose a light, dark or system theme, with live OS sync, saved preference and CSS variable tokens.

Live preview

Open in new tab

Code

A theme picker dropdown built with Tailwind CSS and vanilla JavaScript that lets users choose Light, Dark or System, the three-way switch found in most modern apps and documentation sites. It is shown inside a small notes app so you can see every surface, border and text color change with the selection.

How it works

The root is .tsd-theme. All colors in the markup use arbitrary values such as bg-[var(--tsd-surface)] and text-[var(--tsd-muted)], and style.css defines those custom properties twice: once on .tsd-theme and once on .tsd-theme[data-tsd-theme="dark"]. Switching the theme is therefore a single attribute change, with no dark: variants or Tailwind configuration needed.

The trigger is button.tsd-trigger with aria-haspopup="menu". It shows the icon of the current mode (data-tsd-icon) and its name (data-tsd-label). The #tsd-menu list has role="menu" and three menuitemradio buttons, each carrying data-tsd-value. The check mark and accent icon of the chosen item are driven by group-aria-checked: variants, so the script only sets aria-checked.

On DOMContentLoaded the script reads the saved choice from localStorage (inside try/catch, so it also works where storage is blocked), falls back to data-tsd-mode and calls apply(). In System mode it resolves the theme with matchMedia('(prefers-color-scheme: dark)') and listens for its change event, so the component follows the OS live. The System item's data-tsd-hint shows what the device currently prefers.

Customization

  • --tsd-accent, --tsd-surface and the other tokens in style.css: set your own palette for the light and the [data-tsd-theme="dark"] block.
  • data-tsd-mode on .tsd-theme: change the default from system to light or dark for first-time visitors.
  • Add data-tsd-global to .tsd-theme: the script also toggles the dark class and color-scheme on the html element, so the whole page can follow the choice.
  • STORAGE_KEY in script.js: rename the saved key, or share one key between several pickers.
  • w-60 and right-0 on #tsd-menu: change the menu width or align it to the left edge with left-0.
  • transition-duration: 0.25s in style.css: speed up or slow down the color crossfade.

Accessibility

The trigger exposes aria-haspopup="menu", aria-controls and a live aria-expanded state, plus a visually hidden "Theme:" prefix. Menu items use role="menuitemradio" with aria-checked, so screen readers announce the selected option. Enter, Space, Arrow Down or Arrow Up opens the menu and focuses the current option; Arrow keys, Home and End move between items; Escape closes and returns focus to the trigger; Tab closes the menu and continues; a click outside closes it. Each change is announced through a role="status" region. Focus rings use the accent color, and transitions and the menu animation are removed under prefers-reduced-motion: reduce.

Usage notes

To avoid a flash of the wrong theme on full pages, run the same storage and matchMedia check in a tiny inline script in the head 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.

System, Light and Dark Theme Dropdown – Tailwind CSS theme toggles component screenshot
System, Light and Dark Theme Dropdown · Tailwind CSS Theme Toggles

More Theme Toggles

Light/Dark Theme Toggle – Tailwind CSS theme toggles preview

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.