A $1.2M ARR stat rolls up from $0.0M with the tenths spinning past, a +38% chip springs in and a sparkline draws beneath. Digit strips and an SVG path with pathLength 100.
Also known as: number animation css, count up stat css, animated kpi number, arr counter animation.
<div class="txt-revenue-stat-count-up" aria-label="$1.2M annual recurring revenue, up 38% year over year">
<span class="txt-revenue-stat-count-up-label" aria-hidden="true">Annual recurring revenue</span>
<span class="txt-revenue-stat-count-up-row" aria-hidden="true">
<span class="txt-revenue-stat-count-up-num">$<span class="txt-revenue-stat-count-up-col"><span class="txt-revenue-stat-count-up-ones">01</span></span>.<span class="txt-revenue-stat-count-up-col"><span class="txt-revenue-stat-count-up-tenths">0123456789012</span></span>M</span>
<span class="txt-revenue-stat-count-up-chip">▲ 38%</span>
</span>
<svg class="txt-revenue-stat-count-up-spark" viewBox="0 0 120 26" aria-hidden="true"><path pathLength="100" d="M2 22 L16 19 L28 21 L42 15 L56 17 L70 11 L84 12 L98 6 L114 4"/><circle cx="114" cy="4" r="2.6"/></svg>
</div>.txt-revenue-stat-count-up {
--c: var(--txt-color, #3b82f6);
font-size: calc(var(--txt-size, 32px) * 0.9);
font-family: system-ui, sans-serif;
font-weight: 700;
line-height: 1.2;
display: inline-grid;
gap: 0.2em;
padding: 0.75em 1em 0.7em;
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
border-radius: 0.7em;
background: color-mix(in srgb, currentColor 3%, transparent);
--t: 5.6s;}
.txt-revenue-stat-count-up-label {
font-size: 0.42em;
font-weight: 500;
letter-spacing: 0.01em;
opacity: 0.6;
}
.txt-revenue-stat-count-up-row {
display: flex;
align-items: center;
gap: 0.35em;
animation: txt-revenue-stat-count-up-row var(--t) ease infinite;
}
.txt-revenue-stat-count-up-num {
display: inline-flex;
font-size: 1.9em;
font-weight: 700;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
}
.txt-revenue-stat-count-up-col {
display: inline-block;
height: 1.1em;
overflow: hidden;
line-height: 1.1em;
-webkit-mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent);
mask-image: linear-gradient(transparent, #000 18%, #000 82%, transparent);
}
.txt-revenue-stat-count-up-col > span {
display: block;
width: 1ch;
text-align: center;
word-break: break-all;
}
.txt-revenue-stat-count-up-ones { animation: txt-revenue-stat-count-up-ones var(--t) infinite; }
.txt-revenue-stat-count-up-tenths { animation: txt-revenue-stat-count-up-tenths var(--t) infinite; }
.txt-revenue-stat-count-up-chip {
padding: 0.25em 0.5em;
border-radius: 99em;
font-size: 0.42em;
font-weight: 650;
color: #16a34a;
background: color-mix(in srgb, #22c55e 14%, transparent);
animation: txt-revenue-stat-count-up-chip var(--t) infinite;
}
.txt-revenue-stat-count-up-spark {
width: 5.4em;
height: 1.15em;
overflow: visible;
fill: none;
stroke: var(--c);
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.txt-revenue-stat-count-up-spark path {
stroke-dasharray: 100;
animation: txt-revenue-stat-count-up-draw var(--t) infinite;
}
.txt-revenue-stat-count-up-spark circle {
fill: var(--c);
stroke: none;
transform-box: fill-box;
transform-origin: center;
animation: txt-revenue-stat-count-up-dot var(--t) infinite;
}
@keyframes txt-revenue-stat-count-up-row {
0% { opacity: 0; filter: blur(0.08em); }
5%, 86% { opacity: 1; filter: none; }
94%, 100% { opacity: 0; filter: blur(0.08em); }
}
@keyframes txt-revenue-stat-count-up-ones {
0% { transform: translateY(0); }
5.36% { transform: translateY(0); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
28.57% { transform: translateY(-1.1em); }
95.54% { transform: translateY(-1.1em); }
95.89%, 100% { transform: translateY(0); }
}
@keyframes txt-revenue-stat-count-up-tenths {
0% { transform: translateY(0); }
5.36% { transform: translateY(0); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
35.71% { transform: translateY(-13.2em); }
95.54% { transform: translateY(-13.2em); }
95.89%, 100% { transform: translateY(0); }
}
@keyframes txt-revenue-stat-count-up-chip {
0%, 28% { opacity: 0; transform: scale(0.6); }
33% { opacity: 1; transform: scale(1.08); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
38%, 86% { opacity: 1; transform: none; }
94%, 100% { opacity: 0; transform: none; }
}
@keyframes txt-revenue-stat-count-up-draw {
0%, 6% { stroke-dashoffset: 100; opacity: 1; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
40%, 86% { stroke-dashoffset: 0; opacity: 1; }
94%, 100% { stroke-dashoffset: 0; opacity: 0; }
}
@keyframes txt-revenue-stat-count-up-dot {
0%, 30% { opacity: 0; transform: scale(0); }
36% { opacity: 1; transform: scale(1.5); }
42%, 86% { opacity: 1; transform: scale(1); }
94%, 100% { opacity: 0; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.txt-revenue-stat-count-up, .txt-revenue-stat-count-up *, .txt-revenue-stat-count-up *::before, .txt-revenue-stat-count-up *::after { animation: none !important; }
.txt-revenue-stat-count-up-ones { transform: translateY(-1.1em); }
.txt-revenue-stat-count-up-tenths { transform: translateY(-13.2em); }
}
@media (prefers-reduced-motion: reduce) {
.txt-revenue-stat-count-up, .txt-revenue-stat-count-up *, .txt-revenue-stat-count-up::before, .txt-revenue-stat-count-up::after, .txt-revenue-stat-count-up *::before, .txt-revenue-stat-count-up *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--txt-color on the element or a parent.
Currently previewing #3b82f6.--txt-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 40px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.