Skip to content
animorio
IT
All components

The dark theme switch

Toggles, Transitions, SVGGSAP, CSS onlyMediumCompressed weight: Demo 4.9 KBGSAP 28 KBFreeDownload the zip

A light and dark theme switch, in the minimal style: the knob is a moon that changes phase as it runs on a spring, and the same terminator, as big as the page, sweeps across it from the knob and brings the night or the day.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The dark theme switch</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <!-- the night: a layer the colour of the other theme, opened by a terminator (the edge between light and shadow)
         that sweeps across the page from the knob, the way the shadow sweeps across the moon -->
    <div class="sweep" aria-hidden="true" data-sweep></div>
    <svg class="edge" aria-hidden="true" focusable="false" data-edge><ellipse data-edge-line></ellipse></svg>

    <div class="theme" data-theme>
      <span class="theme__name" id="theme-name">Appearance</span>
      <div class="theme__row">
        <button class="switch" type="button" role="switch" aria-checked="false" aria-labelledby="theme-name theme-word" data-switch data-autoclic>
          <svg class="switch__art" viewBox="0 0 104 56" aria-hidden="true" focusable="false">
            <rect class="switch__track" x="1" y="1" width="102" height="54" rx="27"></rect>
            <g data-knob>
              <circle class="switch__rim" cx="0" cy="0" r="19"></circle>
              <path class="switch__lit" data-lit></path>
            </g>
          </svg>
        </button>
        <span class="theme__word" id="theme-word" aria-hidden="true">
          <span class="theme__w" data-word-light>Light</span>
          <span class="theme__w" data-word-dark>Dark</span>
        </span>
      </div>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

14 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The dark theme switch” from Animorio and fit it into my site

What it does

On a white page, under the word ‘Appearance’, a thin pill-shaped track holding a full black moon, and beside it the word ‘Light’. On a click (or a tap, or Space and Enter) the moon runs to the other end on a spring, goes past by a pixel and comes back; as it runs the shadow crosses it and the moon becomes a thin crescent. Meanwhile, from where the knob is, a hair-thin vertical lens opens and widens sideways, like the edge between light and shadow crossing the moon, and brings the black over the whole page; its edge shows in orange for a moment. As the edge passes the switch, the track, the words and the moon take the new colours; ‘Light’ narrows and thins away, ‘Dark’ comes in wide and light and settles. Another click brings back the day the same way. With the pointer over it the moon leans a little towards the other end; pressed, it shrinks a touch. When it opens the theme follows the system’s.

How it works

The moon as a path

The knob is a thin circle and a path, the lit part, rebuilt on every frame from the lit fraction f: half a circle on the right (the lit limb) and half an ellipse |2f − 1| times the radius wide (the terminator), bulging left when f is over a half and right when it is under. So from 1 (full) to 0.16 (crescent) the shadow crosses it as in the real phases.

The spring and the phase

A single object of numbers (x, phase, wave, press) animated by a GSAP timeline: x with back.out(0.9), which goes past by about a pixel and comes back; the phase with power2.inOut over the same time. The knob's group moves with a transform written on every update of the timeline.

The terminator on the page

The night is a layer in the other theme's colour, cut by a clip-path ellipse worked out in pixels on every frame from the knob's real centre: LENS (3) times as tall as the distance to the farthest edge, as wide as it needs to cover the farthest corner (dx over the square root of 1 − (dy/ry)²) times the progress. At first it is a vertical line, then it opens like a lens; an SVG ellipse with the same centre and radii draws the edge in orange. At the end the page's colours move to the new theme and the layer goes back to zero.

Colours and words

The colours are CSS variables: the page uses those on :root, the switch its own, which GSAP brings to the new colours as soon as the edge passes it. The two words sit on top of each other: the leaving one goes to wdth 62 and wght 300 and fades, the arriving one starts at wdth 125 and wght 260 and settles with back.out.

Accessible

It is a button with role="switch" and aria-checked, labelled by the two words; Space and Enter turn it like a click, and focus shows as an orange ring. The first state comes from prefers-color-scheme and the page also changes color-scheme. A click halfway brings the running turn straight to its end and starts the new one.

Values to change

NameFileDefaultWhat it changes
KNOB_TIMEscript.js0.62How long the knob takes to cross, in seconds.
SPRINGscript.js0.9How far the knob goes past its end (0 not at all; 0.9 is about a pixel).
WAVE_TIMEscript.js0.95How long the terminator takes to cross the page, in seconds.
DARK_PHASEscript.js0.16How much of the moon is lit in the dark theme (1 is a full moon).
LENSscript.js3How tall the sweeping lens is: taller, a straighter edge.
PEEKscript.js3How far the knob leans to the other end under the mouse, in pixels.

Try them in the Playground

When to use it

For the theme switch in any site or app: an app's settings, a blog's header, a portfolio's footer, a dashboard's panel. Wherever whoever turns it should see at once what happens.

On phones

The same with a tap: the switch stays 104 by 56 and the word gets a little smaller; the terminator crosses the narrow screen from where the knob is.

With reduced motion

No run and no lens: the knob and the moon are already in place, the page and the switch move to the other theme in a short fade and the words swap with a fade.

Pitfalls

  • The night widened as a circle, like in the day and night switch that already exists: now it is a lens taller than the page that widens only sideways, the moon's terminator writ large, and it is a different idea.
  • With reduced motion the switch's colours had no value to start from (the variables were only on :root) and jumped: now the variables are written on the switch before every turn.
  • The lens is worked out from the knob's real centre on every frame: worked out once at the start, shrinking the window halfway it no longer covered the corners.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 4.9 KB; the libraries: GSAP 28 KB.

Testing

Tested on 21 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components