Transitions.dev

Home / Transitions

Icon swap

Scale and blur icon swap

Open the live demo Install with the Skill

CSS

Namespaced classes and motion tokens you can retune. The reduced-motion guard ships with the snippet.

/* Transitions.dev — Icon swap */

/*
  Usage:
    <div class="t-icon-swap" data-state="a">
      <span class="t-icon" data-icon="a">…</span>
      <span class="t-icon" data-icon="b">…</span>
    </div>

  State: set data-state="a" or "b" on .t-icon-swap.
  The matching .t-icon fades in; the other fades out with blur
  and scale.
*/

:root {
  --icon-swap-dur: 250ms;
  --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

Self-contained component; the styles inject themselves on first import.

// Transitions.dev — Icon swap (React, self-contained)
// Drop into any React project — no extra CSS file needed.

import { useState } from "react";

// ── Styles ──────────────────────────────────────────────
// Auto-injected on first import. Idempotent (guarded by
// the element id) and SSR-safe (no-ops without document).
const __TRANSITION_STYLES = `
:root {
  --icon-swap-dur: 250ms;
  --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; }
}
`;
if (typeof document !== "undefined" && !document.getElementById("transitions-p5")) {
  const __style = document.createElement("style");
  __style.id = "transitions-p5";
  __style.textContent = __TRANSITION_STYLES;
  document.head.appendChild(__style);
}

// Pair with the CSS from the CSS tab (.t-icon-swap + data-state a/b).
export function IconSwap({ iconA, iconB }) {
  const [state, setState] = useState("a");

  return (
    <button
      type="button"
      aria-label={state === "a" ? "Show B" : "Show A"}
      onClick={() => setState((s) => (s === "a" ? "b" : "a"))}
    >
      <span className="t-icon-swap" data-state={state}>
        <span className="t-icon" data-icon="a">{iconA}</span>
        <span className="t-icon" data-icon="b">{iconB}</span>
      </span>
    </button>
  );
}