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.
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



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>withrole="switch"andaria-checked; a screen reader announces "Light theme, switch, on". The name comes fromaria-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
#e0e0e8track barely stands out from the#f0f0f5frame; 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.
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):
<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 / property | Default | Effect |
|---|---|---|
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
<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.:root { --bg: #0f1117; --text: #e5e7eb } then html.thm-light { --bg: #fff; --text: #1f2937 }, and background: var(--bg) everywhere.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.