Back to Code Bytes
2 min read
Icon Swap

Description

Icon Swap places two icons in the same grid cell and animates between them by fading, blurring, and scaling the outgoing icon down while the incoming icon scales up. It is a good fit for toggle buttons such as sun/moon, play/pause, or expand/collapse where you want a polished cross-dissolve without JavaScript animation libraries.

Example

CSS

:root {
  --icon-swap-dur: 200ms;
  --icon-swap-blur: 2px;
  --icon-swap-start-scale: 0.25;
  --icon-swap-ease: ease-in-out;
}

.t-icon-swap {
  position: relative;
  display: inline-grid;
}
.t-icon-swap .t-icon {
  grid-area: 1 / 1;
  transition:
    opacity var(--icon-swap-dur) var(--icon-swap-ease),
    filter var(--icon-swap-dur) var(--icon-swap-ease),
    transform var(--icon-swap-dur) var(--icon-swap-ease);
  will-change: opacity, filter, transform;
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
  opacity: 1;
  filter: blur(0);
  transform: scale(1);
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
  opacity: 0;
  filter: blur(var(--icon-swap-blur));
  transform: scale(var(--icon-swap-start-scale));
}

@media (prefers-reduced-motion: reduce) {
  .t-icon-swap .t-icon {
    transition: none !important;
  }
}

React

import { useState } from "react";
import { Sun, Moon } from "lucide-react";
import "./icon-swap.css"; // paste the CSS above

export function IconSwap() {
  const [state, setState] = useState<"a" | "b">("a");
  return (
    <button onClick={() => setState((s) => (s === "a" ? "b" : "a"))}>
      <div className="t-icon-swap" data-state={state}>
        <span className="t-icon" data-icon="a">
          <Sun />
        </span>
        <span className="t-icon" data-icon="b">
          <Moon />
        </span>
      </div>
    </button>
  );
}

Variables

VariableDefaultNotes
--icon-swap-dur200mssourced from --p5-dur
--icon-swap-blur2pxsourced from --p5-blur
--icon-swap-start-scale0.25sourced from --p5-start-scale
--icon-swap-easeease-in-outsourced from --p5-ease

Credit

Adapted from Icon Swap on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.