Description
Number Pop-in animates each character of a number independently as it updates, so counters, prices, and balances feel responsive rather than just swapping text. Each digit rises from below with a simultaneous blur-to-sharp entrance. The last two characters (typically a decimal separator and the trailing digit) stagger by --digit-stagger each, giving the decimal portion a slight lag that draws the eye and prevents all digits from arriving at once.
Example
$12.3
CSS
: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
import { useState } from "react";
import "./number-pop-in.css"; // paste the CSS above
function Digits({ value }: { value: string }) {
const chars = value.split("");
return (
<span className="t-digit-group is-animating">
{chars.map((ch, i) => {
const stagger =
i === chars.length - 2
? "1"
: i === chars.length - 1
? "2"
: undefined;
return (
<span key={i} className="t-digit" data-stagger={stagger}>
{ch}
</span>
);
})}
</span>
);
}
export function NumberPopIn() {
const [value, setValue] = useState(12.3);
const [key, setKey] = useState(0);
const update = () => {
setValue((v) => Math.round((v + 7.4) * 10) / 10);
setKey((k) => k + 1);
};
return (
<div>
<span>
$<Digits key={key} value={value.toFixed(1)} />
</span>
<button onClick={update}>Update number</button>
</div>
);
}
Variables
| Variable | Default | Notes |
|---|---|---|
--digit-dur | 500ms | sourced from --p9-dur |
--digit-distance | 8px | sourced from --p9-distance |
--digit-stagger | 70ms | sourced from --p9-stagger |
--digit-blur | 2px | sourced from --p9-blur |
--digit-ease | cubic-bezier(0.34, 1.45, 0.64, 1) | sourced from --p9-ease |
--digit-dir-x | 0 | sourced from --p9-dir-x |
--digit-dir-y | 1 | sourced from --p9-dir-y |
Credit
Adapted from Number Pop-in on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.