Transitions.dev

Home / Transitions

Number pop-in

Digit flip with blur and stagger

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 — Number pop-in */

/*
  Usage:
    <span class="t-digit-group is-animating">
      <span class="t-digit">1</span>
      <span class="t-digit">2</span>
      <span class="t-digit" data-stagger="1">.</span>
      <span class="t-digit" data-stagger="2">3</span>
    </span>

  Replay:
    - Remove `.is-animating`, re-render digits (or swap text),
      force a reflow, then re-add `.is-animating`.
    - Use data-stagger="1", "2", … to delay individual
      digits by `n * var(--digit-stagger)`.

  Direction:
    --digit-dir-x / --digit-dir-y are unit-less multipliers
    (e.g. 1, -1, 0) applied to --digit-distance.
*/

:root {
  --digit-dur: 500ms;
  --digit-distance: 8px;
  --digit-stagger: 70ms;
  --digit-blur: 2px;
  --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
  --digit-dir-x: 0;
  --digit-dir-y: 1;
}

@keyframes t-digit-pop-in {
  0%   {
    transform: translate(
      calc(var(--digit-distance) * var(--digit-dir-x)),
      calc(var(--digit-distance) * var(--digit-dir-y))
    );
    opacity: 0;
    filter: blur(var(--digit-blur));
  }
  100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
}

.t-digit-group {
  display: inline-flex;
  align-items: baseline;
}
.t-digit {
  display: inline-block;
  will-change: transform, opacity, filter;
}
.t-digit-group.is-animating .t-digit {
  animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both;
}
.t-digit-group.is-animating .t-digit[data-stagger="1"] {
  animation-delay: var(--digit-stagger);
}
.t-digit-group.is-animating .t-digit[data-stagger="2"] {
  animation-delay: calc(var(--digit-stagger) * 2);
}

@media (prefers-reduced-motion: reduce) {
  .t-digit-group .t-digit { animation: none !important; }
}

React

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

// Transitions.dev — Number pop-in (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 {
  --digit-dur: 500ms;
  --digit-distance: 8px;
  --digit-stagger: 70ms;
  --digit-blur: 2px;
  --digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
  --digit-dir-x: 0;
  --digit-dir-y: 1;
}

@keyframes t-digit-pop-in {
  0%   {
    transform: translate(
      calc(var(--digit-distance) * var(--digit-dir-x)),
      calc(var(--digit-distance) * var(--digit-dir-y))
    );
    opacity: 0;
    filter: blur(var(--digit-blur));
  }
  100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
}

.t-digit-group {
  display: inline-flex;
  align-items: baseline;
}
.t-digit {
  display: inline-block;
  will-change: transform, opacity, filter;
}
.t-digit-group.is-animating .t-digit {
  animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both;
}
.t-digit-group.is-animating .t-digit[data-stagger="1"] {
  animation-delay: var(--digit-stagger);
}
.t-digit-group.is-animating .t-digit[data-stagger="2"] {
  animation-delay: calc(var(--digit-stagger) * 2);
}

@media (prefers-reduced-motion: reduce) {
  .t-digit-group .t-digit { animation: none !important; }
}
`;
if (typeof document !== "undefined" && !document.getElementById("transitions-p9")) {
  const __style = document.createElement("style");
  __style.id = "transitions-p9";
  __style.textContent = __TRANSITION_STYLES;
  document.head.appendChild(__style);
}

// Pair with the CSS from the CSS tab.
// Toggle `.is-animating` to replay the pop-in sequence.
export function NumberPopIn({ value = "123" }) {
  const [playing, setPlaying] = useState(true);

  // Replay: strip the class, force a reflow via rAF, then re-add.
  const replay = () => {
    setPlaying(false);
    requestAnimationFrame(() =>
      requestAnimationFrame(() => setPlaying(true))
    );
  };

  return (
    <>
      <button type="button" onClick={replay}>Animate</button>
      <span className={"t-digit-group" + (playing ? " is-animating" : "")}>
        {value.split("").map((ch, i) => (
          <span
            key={i}
            className="t-digit"
            data-stagger={i > 0 ? i : undefined}
          >
            {ch}
          </span>
        ))}
      </span>
    </>
  );
}