Persistent Light/Dark Mode Toggle in Webflow (GSAP)

Advanced 30:53 webflowgsapdark-modevariablesjavascript

Build a persistent light/dark toggle in Webflow with variable modes, a GSAP class toggle, smooth color transitions, and localStorage that survives reloads.

Key takeaways

  • A whole theme swap can come down to toggling one class — if you invest up front: define your colors as variables and add a manual "dark mode" variable mode that overrides them.
  • GSAP's set action (not animate) flips that class instantly — set assigns a value with no duration or easing, exactly right for switching a theme.
  • Don't put CSS transitions on every colored element — they'd fight your other GSAP animations. Gate them behind a temporary theme-transition class added at the start of the toggle and removed at the end.
  • Persistence needs a little JavaScript: save the choice to localStorage on click, and read it back in the <head> before paint (also honoring prefers-color-scheme) so there's no flash.
  • Apply the theme class to <html>, not <body> — html exists before the body renders, so the class lands before first paint and the flicker never happens.

Video chapters

  1. 00:00 Intro
  2. 01:28 Defining light and dark mode with variables and variable modes
  3. 07:03 Implementing the color mode toggle button
  4. 09:32 Implementing the light/dark switch interaction
  5. 18:58 Leveraging CSS for a smooth color-mode transition
  6. 21:21 Integrating the smooth transition within the GSAP interaction
  7. 24:41 Leveraging JavaScript for a persistent color-mode switcher
  8. 29:34 Outro

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

The head script reads the saved theme before the page paints:

<!-- Site Settings → Custom Code → Head -->
<script>
/**
 * THEME CHECKER
 * Executed before the body renders to prevent the "white flash" (flicker).
 */
(function () {
  const savedTheme = localStorage.getItem('theme');
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  // Dark if the user chose it before, or has no choice but their system prefers dark
  if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
    document.documentElement.classList.add('dark-mode');
  }
})();
</script>

The footer script saves the choice on each toggle (swap .mode-toggle for your button’s class):

<!-- Site Settings → Custom Code → Before </body> tag -->
<script>
/**
 * THEME TOGGLE MANAGER
 * Handles the click event and persistence.
 */
document.addEventListener("DOMContentLoaded", () => {
  const themeToggle = document.querySelector('.mode-toggle');
  const htmlElement = document.documentElement;

  if (themeToggle) {
    themeToggle.addEventListener('click', () => {
      // Read the class on documentElement to stay consistent with the head script
      const isGoingToDark = !htmlElement.classList.contains('dark-mode');
      if (isGoingToDark) {
        localStorage.setItem('theme', 'dark');
      } else {
        localStorage.setItem('theme', 'light');
      }
    });
  }
});
</script>

How to use it

  1. Set up variables. Create a color variable collection, group your colors, and add a manual variable mode named dark mode with overridden values.
  2. 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.
  3. 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 (target html), toggle play/reverse, runs on all pages.
  4. Add the smooth transition. Paste the theme-transition CSS into the head. In the switch interaction, set theme-transition on at 0s and off at 0.8s. Keep a theme-transition element in your style guide so Webflow keeps the class.
  5. 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).

Both scripts above are the ones from the cloneable — just swap .mode-toggle for your toggle’s class and keep the dark-mode class name in sync.

Resources

In the knowledge base

Reference pages derived from this lesson — the same material reorganised by concept, so you can look one thing up without rewatching. In English.

  • Build a light/dark theme toggle

    Put every themed colour in a variable collection with a second manual variable mode, then the whole theme switch is a GSAP set action toggling one class on the html element; a theme-transition class added for 0.8s smooths the change without colliding with your other animations, and two small scripts make the choice survive a reload.

  • A style cleanup deletes the classes your interactions toggle

    A class that exists only to be added by a GSAP set action is applied to no element, so Webflow flags it as unused and any style cleanup deletes it — publishing itself does not strip it, which means this breaks work that was already correct, later, when someone tidies the project; keep an element wearing the class so it is genuinely used and never appears in a cleanup list.

  • It works in the Designer but not live — or the reverse

    Custom code, non-standard CSS properties, first-paint flicker, scroll lock and keyboard focus all behave differently in the Designer and in plain preview than on the published site, and the Designer additionally shows you your animations' initial states, so half-hidden content while building is expected rather than broken.

  • Verified identifiers

    Every exact attribute name, class name, variable name, SVG id and selector used across the GSAP-in-Webflow corpus, each traced to the cloneable it was copied from; copy these verbatim and never reconstruct one from memory, because a wrong identifier fails silently.

Frequently asked questions

How do I build a dark mode in Webflow with variables?
Create a color variable collection and put every color the theme touches in it, then add a second variable mode (set it to manual) called dark mode and override the values there. A single 'dark-mode' class that applies the dark variable mode can then flip the whole palette at once.
What's the difference between GSAP's set and animate actions?
Animate transitions a property from one value to another over a duration with easing. Set assigns a value instantly — no duration, no easing — and can also add, remove, or toggle a class. For flipping a theme class, set is the right tool.
How do I make the dark-mode color change animate smoothly without breaking my other animations?
Don't add transitions to every element permanently — they'd interfere with your GSAP timelines. Define a theme-transition class (in custom head CSS) that applies color transitions to everything, then use the interaction to add it right before toggling the theme and remove it at the end, so the transition only happens during the switch.
How do I keep a Webflow dark mode from resetting on reload or when changing pages?
Add a little JavaScript: a footer script saves the current choice to localStorage on each toggle, and a head script reads that value before the page paints and applies the theme class (falling back to the system's prefers-color-scheme). Because it runs before paint and targets the html element, the theme persists with no flash of the wrong colors.