← Back to gallery
CSS

Animated Toggle Switches

Three toggle treatments built on real <input type="checkbox"> with appearance: none — a Day/Night celestial pill (sun/moon/stars/clouds), a Neumorphic Bloom dial that morphs into a glowing gemstone, and a Power Glow industrial terminal with a 12-LED boot sequence.

toggle-switchcheckboxappearance-resetrole-switcharia-checkedkeyboard-a11yprefers-reduced-motion

Native checkbox · appearance: none track + thumb

Animated Toggle Switches

Three opinionated toggle studies — Day / Night (a celestial pill with sun, moon, stars, clouds, and rays), Neumorphic Bloom (a tactile pressed-in dial that transforms into a glowing pink gemstone), and Power Glow (an industrial terminal that boots up its 12-LED ring in sequence). Each one is built around a real <input type="checkbox"> with appearance: none so keyboard, screen reader, and focus semantics still come from the native element.

Celestial · sun, moon, stars, clouds

Day / Night

A pill that holds an entire sky. The orb slides + rotates 360° from a glowing sun into a craterd moon, eight rays rotate behind it, two clouds drift across the day side, and ten stars twinkle on the night side. Tap to flip the whole composition; the surrounding stage tints in lockstep.

  • orb 360° flip
  • 10 stars + 2 clouds
  • stage co-shifts

Tactile · pressed surface, dial transform

Neumorphic Bloom

A soft, pressed-in dial. The knob carries grip lines like a real rotary control — until it flips, when it transforms into a glowing pink gemstone with a centred LED ring and three indicator dots that bloom in sequence (0.4s / 0.5s / 0.6s).

  • multi-shadow depth
  • grip → ring
  • 3 dots staggered

Industrial · 12-LED boot sequence

Power Glow

A dormant terminal awakening. Twelve LEDs ignite in clockwise sequence (50ms staggered delays), the core flushes from charcoal to deep emerald, the icon turns into glowing green, and a soft aura ring pulses outward. A numeric readout counts 000 → 100 in tandem.

  • 12 LED boot · 50ms
  • core flush
  • pulsing aura

Toggle inspector

Day / Night

click toggle
  • orb 360° flip
  • 10 stars + 2 clouds
  • stage co-shifts

A pill that holds an entire sky. The orb slides + rotates 360° from a glowing sun into a craterd moon, eight rays rotate behind it, two clouds drift across the day side, and ten stars twinkle on the night side. Tap to flip the whole composition; the surrounding stage tints in lockstep.

Helped you ship something? 🐟 Send my cat a churu

/* A pill that flips an entire sky: orb translateX + 360° rotation, eight rays, ten stars, two drifting clouds, four moon craters. */
.dn {
  width: 220px; height: 92px;
  border-radius: 999px;
  position: relative;
  overflow: hidden;
  background: linear-gradient(160deg, #FEC57A 0%, #F59E0B 45%, #FB923C 100%);
  transition: all 1.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.dn.on {
  background: linear-gradient(160deg, #1E1B4B 0%, #312E81 50%, #3730A3 100%);
}

/* Sun → moon orb: slides 128px AND rotates 360°. */
.dn .orb {
  position: absolute; width: 76px; height: 76px; top: 8px; left: 8px;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 30%, #FFF8DC 0%, #FBBF24 50%, #D97706 100%);
  transition: transform 1.1s cubic-bezier(0.34, 1.56, 0.64, 1),
              background 1s ease;
}
.dn.on .orb {
  transform: translateX(128px) rotate(360deg);
  background: radial-gradient(circle at 32% 30%, #F8FAFC 0%, #E2E8F0 55%, #94A3B8 100%);
}

/* 8 rays rotate behind the sun, fade + scale-out on .on */
.dn .rays { animation: spin 60s linear infinite; transition: opacity 0.6s, transform 1s; }
.dn.on .rays { opacity: 0; transform: translateX(128px) scale(0.5); }

/* Stars only visible on .on, twinkle with staggered delays */
.dn .star { opacity: 0; transition: opacity 0.9s ease 0.4s; }
.dn.on .star { opacity: 1; animation: twinkle 2.6s infinite ease-in-out; }

/* Moon craters fade in 0.6s after the slide */
.dn .crater { opacity: 0; transition: opacity 0.7s ease 0.6s; }
.dn.on .crater { opacity: 1; }

@keyframes twinkle {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50%      { opacity: 1;    transform: scale(1.15); }
}
@keyframes spin { to { transform: rotate(360deg); } }

How to make this

An animated CSS toggle switch keeps a native checkbox as the control, layers a custom track and thumb over it, and moves the thumb with transform for cheap state changes.

html
<label class="toggle-switch">2  <input class="toggle-switch__input"    type="checkbox" role="switch" aria-label="Enable night mode" />  <span class="toggle-switch__track" aria-hidden="true">    <span class="toggle-switch__stars"></span>    <span class="toggle-switch__thumb"></span>  </span></label> <style>.toggle-switch {  position: relative;  display: inline-grid;  width: 4.75rem;  height: 2.35rem;}17.toggle-switch__input {  position: absolute;  inset: 0;  margin: 0;  opacity: 0;  cursor: pointer;}24.toggle-switch__track {  position: relative;  overflow: hidden;  border-radius: 999px;  background: linear-gradient(135deg, #fbbf24, #dbeafe);  box-shadow: inset 0 2px 8px rgba(15, 23, 42, .22);  transition: background .32s ease, box-shadow .32s ease;}.toggle-switch__thumb {  position: absolute;  top: .25rem;  left: .25rem;  width: 1.85rem;  height: 1.85rem;  border-radius: 50%;  background: radial-gradient(circle at 35% 30%, #fff8dc, #f59e0b);  box-shadow: 0 0 18px rgba(251, 191, 36, .55);41  transition: transform .36s cubic-bezier(.34,1.56,.64,1), background .32s ease;}.toggle-switch__input:checked + .toggle-switch__track {  background: linear-gradient(135deg, #111827, #3730a3);  box-shadow: inset 0 2px 10px rgba(0, 0, 0, .42);}47.toggle-switch__input:checked + .toggle-switch__track .toggle-switch__thumb {  transform: translateX(2.4rem) rotate(1turn);  background: radial-gradient(circle at 35% 30%, #f8fafc, #94a3b8);}51.toggle-switch__input:focus-visible + .toggle-switch__track {  outline: 2px solid #67e8f9;  outline-offset: 4px;}55@media (prefers-reduced-motion: reduce) {  .toggle-switch__track,  .toggle-switch__thumb { transition: none; }  .toggle-switch__input:checked + .toggle-switch__track .toggle-switch__thumb {    transform: translateX(2.4rem);  }}</style>

Annotated snippet

  1. Line 2The real checkbox stays in the DOM and carries the switch semantics. The custom art is only a visual layer, so keyboard, forms, and assistive technology keep a native control path.
    PitfallShould a custom toggle switch be a div with role switch?

    Prefer a real input type="checkbox" unless you have a strong reason not to. A checkbox gives forms, keyboard activation, label click behavior, and browser accessibility mappings before the custom CSS layer is added.

  2. Line 17The input covers the full pill while staying transparent. That makes the whole switch clickable without replacing the control with a div.

    A cyan halo marks where a click registers. A thumb-sized input has a tiny target the user has to hunt for. A pill-sized input lets every pixel of the visible control activate the toggle.

    PitfallHow do I make an animated toggle keyboard accessible?

    Wrap it in a label or connect a label with for/id, keep the input focusable, and mirror focus-visible on the visible track. Do not set display: none on the input, because that removes it from keyboard and form flow.

  3. Line 24The track owns the theme transition. Keep background and shadow on the track so the thumb can move independently without forcing a layout recalculation.
  4. Line 41The thumb transition uses transform, not left. transform is composited and can include rotation without changing the switch box or neighboring layout.

    Simulated: animating left on slow devices can drop to ~5fps, looking choppy like the left card. transform sits on the compositor and stays smooth.

    PitfallWhy should the toggle thumb move with transform instead of left?

    left changes a positioned layout value and can become expensive across many controls. transform: translateX() is composited, pairs well with rotation or scale, and does not alter the switch box.

  5. Line 47The checked selector changes the visual state from the input state. Avoid separate JS booleans for the same value unless the component needs controlled form behavior.
  6. Line 51Focus-visible belongs on the visual track because the transparent input is hard to see. The outline still follows native keyboard focus.
    PitfallHow do I make an animated toggle keyboard accessible?

    Wrap it in a label or connect a label with for/id, keep the input focusable, and mirror focus-visible on the visible track. Do not set display: none on the input, because that removes it from keyboard and form flow.

  7. Line 55Reduced motion removes animated easing while preserving the checked position. The control still communicates on and off through color and thumb placement.
    PitfallDo animated switches need prefers-reduced-motion?

    Yes. Remove springy travel, rotation, particles, and looping decoration under prefers-reduced-motion. Keep the checked and unchecked states visually distinct with position, color, or text.

Other pitfalls

Are appearance: none toggles reliable across browsers?
They are broadly usable, but native input styling differs. The safest pattern is to make the input transparent and layer the custom track as a sibling, so the visuals do not depend on vendor-specific checkbox painting.

Notes

Overview

Toggle switches built on a real <input type="checkbox"> with appearance: none so keyboard, screen-reader, and focus semantics still come from the native element. Three variants demonstrate the technique’s range: a Day/Night celestial pill, a neumorphic dial morph, and an industrial 12-LED power glow.

When to use it

Reach for toggle switches when the option is binary and the change applies immediately (no separate save action) — notification settings, dark mode, in-app feature flags. Skip them for binary decisions that need an explicit confirm (use checkbox + save button) and for non-binary options (use segmented control or radio).

How it works

The native <input type="checkbox" role="switch"> gets appearance: none to strip its default rendering. A wrapping <label> draws the track and thumb via a ::before (track background) and ::after (thumb). When :checked, the wrapper toggles a CSS custom property like --on: 1 via input:checked + label { --on: 1 } which the thumb animation reads: thumb transform: translateX(calc(var(--on) * var(--track-width))) slides between endpoints. Variant decorations (sun/moon, LEDs, neumorphic depth) attach as extra pseudo-element layers driven by the same state property.

Production gotchas

Forgetting role="switch" on the input means screen readers announce “checkbox” instead of “switch” — the semantic difference is small but real for AT users. The visually-hidden native input must still capture focus — do not use display: none or visibility: hidden; use opacity: 0; position: absolute so focus and click events still reach it. Thumb animations on tightly packed switches should clamp the thumb size at least 2px smaller than the track inset or rounding pushes the thumb outside the track on retina.

Accessibility

Native checkbox + role="switch" gives screen readers the right announcement and keyboard support (Space toggles) for free. Add an aria-label describing what the switch controls if the visible label is decorative-only. Under prefers-reduced-motion: reduce drop the slide transition so the thumb snaps to position. Verify focus ring visibility on both on and off states — the focused state must read distinct from the track.

References

Implementation depth

The checkbox or switch state should be the source of truth. The knob translation, track color, and icon swap are all visual reflections of a state that keyboard and assistive tech can already operate.

Do not hide focus behind the moving knob. A switch needs visible keyboard focus on the control itself, a label that explains the setting, and reduced motion that snaps the knob while keeping the state obvious.