Utilities✨ Premium

Theme Toggle

Sun / moon switch that really changes the theme: from the demo frame to the whole page, with the device setting and a remembered choice as options.

CSSToggleTransition

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Utilities has 43 effects, including 3 free. Explore the category →

3 usage examples

A whole API documentation goes from dark to light — Theme Toggle example 1

① A whole API documentation goes from dark to light

WhenThe developer reads the "Create a payment" page in dark mode at night; the next day at the office, they flip the switch next to the search box: menu, text, code block and header all turn light together.
WhyThis is what a theme switch is for: it does not colour a frame, it changes how the whole site looks. The class set on a single container is enough, every light-theme colour being written in the site's CSS.
Settingsdata-thm-target="#u737-doc" (the page container); demo frame neutralised (--thm-height: 0, --thm-page, --thm-panel and --thm-panel-light transparent). The light colours are .thm-light rules of the scene. In the capture, the scene calls root.fx0737.toggle() every 1.3 s.
A restaurant terrace, by day or by night — Theme Toggle example 2

② A restaurant terrace, by day or by night

WhenOn La Ferlinière's booking page, a guest is unsure about dining outside: the switch on the photo shows the same terrace in broad daylight, then in the evening with its string lights on.
WhyThe icon follows the action: the moon invites you to see the night, the sun to go back to day. The same component becomes a preview picker, with no code beyond the two states of the illustration.
Settingsdata-thm-target="#u737-terrace" (the photo block only), aria-label="Day view of the terrace", switch in a dark pill at the top left. CSS illustration whose colours change under .thm-light. Automatic toggle every 1.3 s in the capture.
Day trains or night trains in a departures board — Theme Toggle example 3

③ Day trains or night trains in a departures board

WhenOn a station's departures board, the traveller flips the small switch in the header: the list of night trains (couchettes, sleepers) gives way to the morning departures, and the board turns light.
WhyHere the switch filters a list: the class set on the board shows one list or the other on top of changing its colours. The accessible name says what the button does ("Day trains"), not "theme".
Settingsdata-thm-target="#u737-board", aria-label="Day trains"; in the scene, .thm-light .u737-day shows and .thm-light .u737-night hides. Toggle every 1.3 s in the capture.

How it works

The switch is a real <button role="switch">. On click, Enter or Space, aria-checked goes from false to true: the knob slides right with a slight overshoot (cubic-bezier(.68,-.55,.265,1.55), 0.4 s), the sun spins away and the moon appears. true means light theme.

At the same time, the script adds the thm-light class to a target and removes it when going back to dark. With no setting, the target is the .thm-frame frame around the switch (it goes from #1a1a2e to #f0f0f5). With data-thm-target="html" on the root, the whole page gets the class: your own html.thm-light { … } rules do the rest. The class name is set with data-thm-class.

On load, the starting state is dark, unless the target already has the class (set by your server, for instance). data-thm-start="system" reads prefers-color-scheme and follows changes of the device setting until the visitor makes a choice; data-thm-start="light" starts light. data-thm-storage="theme" saves the choice in localStorage under that key and restores it on the next visit; without this attribute, nothing is written on the device.

Each .fx-0737-root is independent; two switches aiming at the same target (header and footer) stay in sync. Every toggle fires the fx0737:change event (detail.light), and root.fx0737.set(true), toggle(), isLight() drive the switch from your code.

Accessibility

  • Role and state: a <button> with role="switch" and aria-checked; a screen reader announces "Light theme, switch, on". The name comes from aria-label, to translate or adapt in the HTML. Sun and moon are decorative (aria-hidden).
  • Keyboard: Tab to reach it, Space or Enter to toggle; lavender ring on keyboard focus (:focus-visible). In Safari on Mac, Tab only stops on buttons if the system keyboard option is on (otherwise Option + Tab).
  • Contrast: in light mode the #e0e0e8 track barely stands out from the #f0f0f5 frame; the indigo knob is what shows the state. On a very light page, give the track a darker border.
  • Reduced motion: under prefers-reduced-motion: reduce, no more sliding, spinning or fading; the state changes in place and the theme switch works the same.

Browser compatibility

A button, CSS and 3.5 KB of dependency-free JavaScript, comments included (classList, CustomEvent, matchMedia, localStorage). Measured in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): code pasted twice in a page, each copy toggles on its own, keyboard, touch at 390 px, reduced motion, html target shared by two switches, choice kept after reload, 0 errors, no name added to window.

Chrome 86+✓ Full (measured)
Firefox 85+✓ Full (measured)
Safari 15.4+✓ Full (measured in WebKit)
Edge 86+✓ Full (Chromium engine)
Mobile iOS 15.4+✓ WebKit engine, not checked on a device
Android Chrome 86+✓ Full (touch measured)

Without JavaScript, the button shows but toggles nothing. Before Chrome 86, Firefox 85 and Safari 15.4 (no :focus-visible), everything works but without a focus ring. data-thm-start="system" needs prefers-color-scheme (Chrome 76, Firefox 67, Safari 12.1); elsewhere the start stays dark.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0737-root" data-thm-target="html" data-thm-start="system" data-thm-storage="theme">
  <div class="thm-frame">
    <button type="button" class="thm-switch" role="switch" aria-checked="false" aria-label="Light theme">
      <span class="thm-knob" aria-hidden="true">
        <span class="thm-sun">☀</span>
        <span class="thm-moon">☾</span>
      </span>
    </button>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Target (HTML — data-thm-target on the root) the .thm-frame frame CSS selector of the element that gets the class: html for the whole site, #app for one area.
Light theme class (HTML — data-thm-class) thm-light Present in light mode, absent in dark. Use the one your CSS already relies on (light, is-light…).
Start (HTML — data-thm-start) dark light starts light; system reads prefers-color-scheme and follows its changes until the visitor's first choice.
Memory (HTML — data-thm-storage) absent: nothing is stored localStorage key (e.g. theme); the light or dark value is restored on the next visit and wins over data-thm-start.
Spoken name (HTML — the button's aria-label) "Light theme" Say what the on state shows: "Day view", "Light mode"…
Demo frame (CSS — --thm-height, --thm-page, --thm-panel, --thm-panel-light, --thm-frame-height) 300 px · #0a0a0f · #1a1a2e · #f0f0f5 · 120 px Set 0 and transparent to place the switch alone in a header.
Control (JS — root.fx0737 and the fx0737:change event) — set(true) forces light, toggle() flips, isLight() reads the state; the event gives detail.light on every toggle.

FAQ

The script runs after the HTML is shown. Add one line in the <head>, before your styles: <script>try{if(localStorage.getItem('theme')==='light')document.documentElement.classList.add('thm-light')}catch(e){}</script>, with the same key as data-thm-storage and the html target.
In your CSS, under the class set on the target. The simplest way is variables: :root { --bg: #0f1117; --text: #e5e7eb } then html.thm-light { --bg: #fff; --text: #1f2937 }, and background: var(--bg) everywhere.
Yes with data-thm-start="system", as long as they have not touched the switch and no choice is stored. Once they choose, their choice wins over the device setting.