A good light/dark toggle in Webflow isn’t one feature — it’s a small system: a color palette built on variables, a GSAP interaction that flips a single class, a smooth color transition that doesn’t sabotage your other animations, and a touch of JavaScript so the choice survives reloads and page changes. Build it once, the right way, and you can drop it into any project.
The payoff of doing the groundwork is almost magical: an entire theme change comes down to toggling one class. This lesson is also a great look at GSAP’s set action — the instant, no-duration cousin of animate — and how it plays with Webflow’s variable modes.
How it works
Variables and variable modes are the foundation. Put every color the theme should affect into a variable collection (grouped, e.g. backgrounds and fonts). The default base mode is your light theme; add a second manual variable mode called dark mode and override the values there. Then connect those variables to everything that uses color — start at the Body (All Pages) tag for font and background (most elements inherit from it), then specific classes and states as needed. With that done, applying a dark-mode class that carries the dark variable mode flips the whole palette instantly.
GSAP’s set action flips the class. The toggle uses two interactions, both with control toggle play/reverse (so each click reverses): one animates the little switch knob across the track, the other uses a set action on the class. Set is different from animate — instead of an “animated properties” section it has set properties, and under it a class option with add / remove / toggle. Toggling the dark-mode class is the entire theme switch. (Set runs on the page you built it — remember to set runs on: all pages so a copied toggle works everywhere, and keep the dark-mode class attached to something in your style guide so Webflow doesn’t prune it as unused.)
Smoothing the change without breaking everything. By default the theme flips instantly. You could add CSS transitions to every colored element, but that’s impractical and worse — those transitions would collide with any GSAP scroll/hover animation touching the same properties. The smarter move is a theme-transition class defined in custom head CSS that applies color transitions to all elements only while it’s present:
<!-- Site Settings → Custom Code → Head -->
<style>
.theme-transition,
.theme-transition *,
.theme-transition *::before,
.theme-transition *::after {
transition:
color 0.8s cubic-bezier(0.16, 1, 0.3, 1),
background-color 0.8s cubic-bezier(0.16, 1, 0.3, 1),
border-color 0.8s cubic-bezier(0.16, 1, 0.3, 1),
outline-color 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
</style>
In the switch interaction, set the theme-transition class on at 0s and set it off at 0.8s. Now colors ease over 800ms during the switch, and the transition is gone the rest of the time so it never interferes with anything else.
Persistence needs JavaScript — and this is where the flicker fix from lesson 2 returns in spirit. Two small scripts in Site Settings: a head script that runs before paint, reads the saved theme from localStorage (falling back to the system prefers-color-scheme), and applies the dark-mode class; and a footer script that saves the current choice to localStorage on each toggle. Critically, the persistence class goes on the <html> element, not <body> — html exists before the body renders, so applying the class before first paint means no flash of the wrong theme. (For that reason, switch the toggle’s set-class target from body to html too.)
How to use it
- Set up variables. Create a color variable collection, group your colors, and add a manual variable mode named dark mode with overridden values.
- Wire colors to the system. Assign the variables at the Body (All Pages) level first, then to specific classes/states so everything the theme touches is variable-driven.
- Build the toggle interactions. One interaction moves the switch knob (animate X, toggle play/reverse). Another uses a set → class → toggle action on
dark-mode(targethtml), toggle play/reverse, runs on all pages. - Add the smooth transition. Paste the
theme-transitionCSS into the head. In the switch interaction, settheme-transitionon at 0s and off at 0.8s. Keep atheme-transitionelement in your style guide so Webflow keeps the class. - Add persistence. Add the head + footer scripts (from the cloneable) for localStorage + prefers-color-scheme, then make the toggle a component and reuse it across pages. Publish and test on the live link (custom code doesn’t always run in preview).
The exact JavaScript (head persistence + footer save) lives in the cloneable — grab it there and match your class/variable names rather than retyping it.