Transitions✨ Premium

Icon Toggle

A sun and a moon stacked in the center of a 72 px round button: on click, one turns from −90° to 0° while growing, the other leaves at 90° while shrinking, in 0.5 s, and the label below changes its text. It is the switch's micro-interaction, not the page switch; 0.6 KB of JavaScript, a 150 ms fade with no rotation when the user reduces motion.

CSSIconMorph

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →

3 usage examples

Day program to night program — a boiler thermostat — Icon Toggle example 1

① Day program to night program — a boiler thermostat

WhenIn the evening, the resident switches from the day program (sun, 20 °C set point) to the night program (moon, 17 °C set point) on the boiler's wall screen.
WhyOnly the button changes: the sun turns and gives way to the moon, which confirms the program without redrawing the screen; the dial and the figure follow, the eye knows where to look.
SettingsReversed direction: the sun is the starting state (active set in the HTML), the moon the switched state; button enlarged to 88 px, 38 px icons; 'Day program · 20 °C' and 'Night program · 17 °C' labels in data-on / data-off.
Forced street lighting — a town's technical services dashboard — Icon Toggle example 2

② Forced street lighting — a town's technical services dashboard

WhenDuring a storm warning, the operator forces the lights on in Rue des Cordiers: the sun (lights off) turns into a moon (lights on), in a list of forty streets.
WhyThe rotation gives clear feedback on a small button, readable even in the middle of the list; the 'On · forced' label next to it confirms in plain words.
SettingsOne button per row, shrunk to 42 px with 22 px icons (the quarter turn stays visible); the row that switches in the demo has a 62 px button (32 px icons); label placed left of the button on the same line; the effect's script arms all four buttons in the list with no setup.
Awake to asleep — a baby's sleep log — Icon Toggle example 3

③ Awake to asleep — a baby's sleep log

WhenThe parent, baby in arms, logs the 7:45 pm bedtime one-handed: the 'woke up at 7:10 am' sun turns into the 'down at 7:45 pm' moon.
WhyThe turning icon is enough as a confirmation: a glance tells what was just recorded, without rereading the list.
Settings86 px button at the bottom of the screen, in the thumb zone; dark brown background at night, golden cream by day; bold label under the button.

How it works

The .th-icon-btn button is a real 72 px round <button>, 5% white background and a 2 px 10% white border. It holds two 32 px stroke-only SVG icons (fill: none, 1.5 width): .th-icon-sun, in #fbbf24, and .th-icon-moon, in #94a3b8. Both are position: absolute in the same spot, centered by the flex button: one replaces the other with nothing shifting. At rest (dark theme), the moon is upright and full (rotate(0deg) scale(1), opacity 1); the sun waits turned −90°, half size and invisible.

The .active class swaps the roles: the sun comes to rotate(0deg) scale(1) and opacity 1, the moon leaves to rotate(90deg) scale(.5) and opacity 0, both with transition: opacity .5s, transform .5s on the cubic-bezier(.4, 0, .2, 1) curve. The two icons therefore turn in the same direction, clockwise, like a disc rotating a quarter turn: one fades out as it turns while the other arrives. The button takes a 10% amber background and a 30% amber border (background-color .5s, border-color .5s). Measured 300 ms after the click: sun at 0.89 opacity in Chromium 151 and WebKit 26, 0.88 in Firefox 153, turned about −10°; everything is in place at 0.5 s, and the second click plays the quarter turn backwards.

The script (0.6 KB) arms every .th-icon-btn on the page, with no id and no global function: on click, it toggles .active, updates aria-pressed and writes into the neighboring .th-icon-label (in the same .th-icon-scene) the text of its data-on or data-off attribute — 'Light mode' / 'Dark mode' here. Labels thus live in the HTML, translatable without touching the script. The effect animates the button and its label, and nothing else: it does not change the page theme, does not read prefers-color-scheme and remembers nothing. Hook your own theme change onto the same click (or read aria-pressed).

Accessibility

  • Button: a <button type="button"> named 'Light mode' (aria-label, the name does not change) with aria-pressed set to true when the sun shows; both icons are aria-hidden. Measured: reached with the Tab key in Chromium, toggled by Enter and Space in Chromium, Firefox and WebKit; a 2 px violet outline on keyboard focus.
  • Reduced motion: under prefers-reduced-motion: reduce, no more rotation or scaling (transform: none), the two icons hand over with a 150 ms opacity fade. Measured: sun at 0.39 (Chromium) and 0.33 (Firefox) opacity 60 ms after the click, 1 at 300 ms, with no transform.
  • Contrast: label in 50% white on #0a0a0f at 5.3:1; the icons, as graphics, exceed 3:1 on the button background (amber #fbbf24 and gray #94a3b8 on black). The text label doubles the icon: the state does not rely on the sun or moon shape alone.

Browser compatibility

CSS (transform and opacity transitions, :focus-visible) + 0.6 KB of JavaScript (closest(), data- attributes), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: the same values at 60 and 300 ms within a few hundredths, same way back, French and English labels. transform transitions are universal; closest() exists since Chrome 41, Firefox 35 and Safari 6; :focus-visible since Chrome 86, Firefox 85 and Safari 15.4 (before that, the default focus ring stays). The minimum versions come from gap in flex boxes: Chrome and Edge 84, Firefox 63, Safari 14.1 (iOS 14.5); before that, the items touch.

Chrome 84+✓ Full
Firefox 63+✓ Full
Safari 14.1+✓ Full
Edge 84+✓ Full
Mobile iOS 14.5+✓ Full
Android Chrome 84+✓ Full

Without JavaScript, the button stays on the moon with the 'Dark mode' label and does not react; if your page switches theme on the server, set active and aria-pressed="true" in the HTML to show the sun directly.

The code

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

index.html — structure
<div class="th-icon-root">
  <div class="th-icon-scene">
    <button class="th-icon-btn" type="button" aria-pressed="false" aria-label="Light mode">
      <span class="th-icon-sun" aria-hidden="true"><svg>…</svg></span>
      <span class="th-icon-moon" aria-hidden="true"><svg>…</svg></span>
    </button>
    <span class="th-icon-label" data-on="Light mode" data-off="Dark mode">Dark mode</span>
  </div>
</div>
Unlock the full code — $9.90/mo incl. tax

The full HTML + CSS + JS of Icon Toggle, ready to paste — plus all 840 effects. Or $299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Duration and curve (CSS — transition on .th-icon-sun / .th-icon-moon) .5s cubic-bezier(.4, 0, .2, 1) 0.35 s for a snappy switch; beyond 0.7 s, users wait for the end before moving on.
Exit angle and scale (CSS — rotate(-90deg) scale(.5) and rotate(90deg) scale(.5)) quarter turn, half size 180° gives a more striking spin; scale(0) makes the icon vanish into the center. Keep opposite signs so both turn the same way.
Size (CSS — .th-icon-btn and svg) 72 px and 32 px Keep the icon at about 45% of the button: 42 / 22 px in a list, 88 / 38 px for a wall screen.
Labels (HTML — data-on and data-off on .th-icon-label) 'Light mode' / 'Dark mode' Put the real state there ('Night heating · 17 °C'); remove .th-icon-label if the button stands alone, the script does without it. Update aria-label too.
Colors (CSS — icon stroke, background and border of .th-icon-btn.active) sun #fbbf24, moon #94a3b8 On a light background, darken both strokes (#d97706, #475569) to keep 3:1.
Reduced motion (CSS — @media (prefers-reduced-motion: reduce) block) 150 ms fade, no rotation transition: none for an instant swap.

FAQ

No: it animates its icon, updates aria-pressed and its label, nothing else. To change the theme, add your own code to the same click, for instance document.documentElement.classList.toggle('light', btn.classList.contains('active')), or pair it with another switch from this category. The effect does not read prefers-color-scheme either and remembers nothing: if the page starts light, set active and aria-pressed="true" in the HTML.
An SVG path does not interpolate from one shape to another without a dedicated tool. Two icons in the same spot, each with its own opacity and rotation, give a continuous handover with two CSS transitions. As they are position: absolute, the button keeps its size and nothing around it moves.
Yes: the script arms every .th-icon-btn it finds and looks for the label in the surrounding .th-icon-scene, with no id. A list of forty streets with one button each works as is; keep one .th-icon-scene per button-label pair.