GODRICH

9 CSS Dark Mode Toggle Animations, One Line Each

A css dark mode toggle flips a site's color scheme when it's switched, usually by toggling one class or custom property.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order runs from the most familiar shape to the least. 01, 02, 06, and 07 are variations on a physical switch — an icon, a knob, a card, a set of rays — the kind of control a reader already expects to find in a header. 03 and 05 spend that familiarity on a bigger reveal, sweeping the whole panel into its new scheme instead of just flipping one small shape. 04 sets the icon aside entirely and shows the one CSS value doing the real work underneath any of the eight others, and 08 and 09 close it out — one page-wide fade, one switch with a third option beyond just light and dark.

01Sun/moon icon swap

A sun icon rotates and fades out while a moon icon fades in behind it at the same time, the two shapes cross-fading rather than one path morphing into the other. It's the toggle shape most readers already recognize from browser chrome and OS settings, which is why it reads instantly in a header or a settings menu.

400ms회전 180°크로스페이드
.toggle__sun {
  background: $stage-yellow;
  transform: rotate(0deg);
  animation: sun $duration $easing infinite;
}
.toggle__moon {
  background: $subject-cream;
  opacity: 0;
  transform: rotate(-90deg);
  animation: moon $duration $easing infinite;
}
@keyframes sun {
  0%   { opacity: 1; transform: rotate(0deg); }
  50%  { opacity: 0; transform: rotate(90deg); }
  100% { opacity: 1; transform: rotate(0deg); }
}

02Sliding knob

A round knob slides across a pill-shaped track while the track's own background color shifts underneath it, so the switch reads as one continuous move instead of two separate changes happening at once. It suits a settings screen or the right edge of a nav bar — anywhere a toggle switch is already the shape a reader expects.

28px 트랙느리게 600ms그림자 추가
.knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: $color;
  transform: translateX(0);
  animation: knob-slide $duration $easing infinite;
}
@keyframes knob-slide {
  0%   { transform: translateX(0); }
  50%  { transform: translateX(20px); }
  100% { transform: translateX(0); }
}

03Circular reveal via View Transitions

A circle grows outward from the toggle and sweeps the whole panel into its new color scheme in one motion, the same silhouette the View Transitions API draws when it clips a snapshot of the old page while the new one grows in behind it. It earns its extra weight on a landing hero or an app's first screen, somewhere a single dramatic switch is worth noticing.

중앙에서1.2배속사각 확산
.wipe {
  position: absolute;
  top: -60px;
  right: -60px;
  width: 320px;
  height: 320px;
  border-radius: 50%;
  background: $color;
  transform-origin: 78% 22%;
  transform: scale(0);
  animation: wipe-scale $duration $easing infinite;
}
@keyframes wipe-scale {
  0%   { transform: scale(0); }
  50%  { transform: scale(1); }
  100% { transform: scale(0); }
}

04One-line light-dark() switch

Two swatches swap places to stand in for what a single color-scheme declaration does site-wide: once it's set, the light-dark() function picks the right value for background, text, or a border in one line each, with nothing else to toggle by hand. It's built for a component library or a design-system token file, where dozens of colors need to flip together and none of them should need their own media query.

변수 2개전환 300ms테두리 포함
.chip__swatch--light {
  background: $color;
  opacity: 1;
  animation: face-light $duration $easing infinite;
}
.chip__swatch--dark {
  background: $stage-yellow;
  opacity: 0;
  animation: face-dark $duration $easing infinite;
}
@keyframes face-light {
  0%, 15%   { opacity: 1; }
  40%, 60%  { opacity: 0; }
  85%, 100% { opacity: 1; }
}

05Eclipse mask transition

A dark, moon-shaped disc slides in from the side and settles directly over a sun icon, the two shapes overlapping like an eclipse instead of one simply fading into the other. It's built for a toggle that wants to be noticed — a brand's own dark-mode button, or a corner of a product page introducing the feature for the first time.

500ms그림자 진하게역방향 재생
.sun {
  position: absolute;
  bottom: 16px;
  left: 18px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: $color;
}
.mask {
  position: absolute;
  bottom: 12px;
  left: 4px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: $stage-ink;
  transform: translateX(-60px);
  animation: mask-slide $duration $easing infinite;
}
@keyframes mask-slide {
  0%, 15%   { transform: translateX(-60px); }
  40%, 60%  { transform: translateX(4px); }
  85%, 100% { transform: translateX(-60px); }
}

06Flip-card transition

The whole toggle is a card with two faces glued back to back inside a perspective wrapper, and rotateY spins it from a sun face to a moon face and back again, the same motion as flipping a coin. It works as a card-style setting switch or inside a profile menu, wherever the control already looks like a small object rather than a flat rectangle.

600msperspective 800px그림자 따라 이동
.card {
  perspective: 900px;
}
.card__inner {
  position: relative;
  transform-style: preserve-3d;
  animation: flip $duration $easing infinite;
}
.card__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}
.card__face--back {
  transform: rotateY(180deg);
}
@keyframes flip {
  0%, 15%   { transform: rotateY(0deg); }
  50%       { transform: rotateY(180deg); }
  85%, 100% { transform: rotateY(360deg); }
}

07Icon morph transition

Six rays fold flat around a central circle while its color and opacity shift underneath them, so the sun's points retract into what reads as one of the moon's craters instead of two separate icons trading places. It's built for an icon-only button — a toolbar toggle where a label doesn't fit and the icon has to do all the explaining on its own.

SVG path350mseasing 스냅
.icon__ray {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3px;
  height: 8px;
  border-radius: 2px;
  background: $color;
  transform-origin: 50% 17px;
  animation: ray-fold $duration $easing infinite;
}
@keyframes ray-fold {
  0%, 15%   { opacity: 1; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(1); }
  40%, 60%  { opacity: 0; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(0); }
  85%, 100% { opacity: 1; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(1); }
}

08Whole-page color fade

Background and text sit as two stacked layers that cross-fade on the same curve, so the page trades one full color scheme for the other instead of any single property being interpolated in place. It fits a full document view — a reader, a viewer, or anywhere the whole canvas needs to change at once rather than one button doing it alone.

800ms필터 blur 2px즉시 전환
.panel__face--light {
  background: $subject-cream;
  color: $subject-ink;
  opacity: 1;
  animation: face-light $duration $easing infinite;
}
.panel__face--dark {
  background: $stage-ink;
  color: $subject-cream;
  opacity: 0;
  animation: face-dark $duration $easing infinite;
}
@keyframes face-light {
  0%, 15%   { opacity: 1; }
  40%, 60%  { opacity: 0; }
  85%, 100% { opacity: 1; }
}

09Light/dark/auto 3-state

A pill-shaped track with three slots moves a single indicator thumb through light, dark, and auto in sequence, translateX doing all the work instead of three buttons standing in for three states. It belongs on a detailed settings page or a desktop app's preferences, anywhere a reader expects a third option beyond a plain on/off.

3단 트랙350ms인디케이터 그림자
.segmented__thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 22px;
  border-radius: $r-pill;
  background: $color;
  transform: translateX(0);
  animation: thumb-move $duration $easing infinite;
}
@keyframes thumb-move {
  0%, 22%  { transform: translateX(0); }
  33%, 55% { transform: translateX(32px); }
  66%, 88% { transform: translateX(64px); }
  100%     { transform: translateX(0); }
}

Where it breaks — the traps

All nine flip from light to dark and back to light on their own inside the grid above, on a fixed two-second loop — there's nothing to click up there, only something to watch, because a fixed-size iframe with no real page behind it has no scroll position or click target to react to. The loop stands in for the interaction; the vanilla files in the zip are the ones that actually respond to a press.

A second trap catches anyone who only recolors the background and the text and stops there: without setting color-scheme on the root element, native form controls, scrollbars, and even the browser's own UI keep rendering light, so the page looks only half switched no matter how dark the body itself gets. 04's light-dark() approach depends on that same property being declared first — the function reads color-scheme to decide which of its two values to hand back — and its own support trails behind it: Chrome ships light-dark() from version 123, Safari from 17.5, and Firefox from 120, well after color-scheme itself, which every modern browser already understands. Anyone shipping 04 to production should open the zip and read style.css directly, and it unlocks with kjdwbr3q typed in by hand rather than guessed.

The third trap is easy to miss on 03 and 05 specifically: both position their moving shape with an absolute top/left/right pulled straight from the demo's fixed panel size, so dropping either one into a wider real card without adjusting those offsets leaves the circle or the mask centered on the wrong spot entirely.

Accessibility

Every one of the nine respects prefers-reduced-motion, but the fallback isn't the same shape twice. 01, 02, 05, 06, and 09 just stop moving and settle on whichever state they were already in — the sun stays a sun, the knob stays put — since nothing about those five is decorative enough to keep once motion is off. 03's circle never grows and 07's rays never fold; both jump straight to the finished shape instead of playing the reveal. MDN's guide to prefers-reduced-motion covers how widely browsers support the query itself.

@media (prefers-reduced-motion: reduce) {
  .toggle__sun, .toggle__moon, .knob, .wipe, .mask,
  .card__inner, .icon__ray, .segmented__thumb { animation: none !important; }
}

Nine more patterns like these live in the CSS category, and what this site actually ships gets explained on the about page.

FAQ

Do I need JavaScript for a css dark mode toggle?

Not for the motion itself. Every animation in this set runs on CSS transforms and opacity, so a css dark mode switch without javascript is entirely possible for the transition itself — you still need one line of JS (or a form control) to actually flip a class or attribute, since CSS alone can't remember a user's choice between visits.

What's the difference between a dark mode toggle and a dark mode switch?

Nothing structural — "toggle" and "switch" describe the same two-state control, and search engines treat dark theme toggle css and css dark mode switch as the same query. The naming difference usually just tracks which shape the design uses: a switch tends to look like 02's sliding knob, while a toggle covers icon-based versions like 01 and 07 too.

Can I add a css dark mode toggle button to an existing site without a redesign?

Yes, as long as the site's colors already route through custom properties. Any of these nine can sit inside a header as an html css dark mode toggle button on its own, and swapping the two color values it touches is a smaller change than restyling every component by hand.

Enter the archive password

The password is inside this article. You will find it as you read.