Introduction
Static data is boring. When a user arrives on a page with statistics, KPIs or metrics, animation makes all the difference. It captures attention, makes information memorable and adds a touch of professionalism to your interface.
In this tutorial, we will explore 5 data animation techniques: incrementing counters, circular progress bars, animated charts, statistics cards and gauges. Each example is accompanied by functional ready-to-use code.
All demos use the Intersection Observer API to trigger animations only when the element becomes visible. This improves performance and user experience.
1. Animated Counter
The animated counter is the most common effect for displaying key figures. The number increments progressively from 0 to the target value, creating an engaging "counting" effect.
<!-- HTML -->
<div class="counter"
data-target="12847"
data-suffix="+">
0
</div>
<!-- CSS -->
.counter {
font-size: 4rem;
font-weight: 900;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
<!-- JavaScript -->
function animateCounter(element) {
const target = parseFloat(element.dataset.target);
const suffix = element.dataset.suffix || '';
const decimals = parseInt(element.dataset.decimals) || 0;
const duration = 2000;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function (ease-out)
const eased = 1 - Math.pow(1 - progress, 3);
const current = eased * target;
element.textContent = current.toFixed(decimals) + suffix;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// Trigger on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
document.querySelectorAll('.counter').forEach(el => observer.observe(el));
2. Circular progress bar
Circular progress bars are perfect for displaying percentages or scores. Using SVG with stroke-dasharray allows precise control of the animation.
<!-- HTML -->
<div class="circular-progress" data-value="75">
<svg width="150" height="150" viewBox="0 0 150 150">
<defs>
<linearGradient id="gradient">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient>
</defs>
<circle class="bg" cx="75" cy="75" r="65"/>
<circle class="progress" cx="75" cy="75" r="65"/>
</svg>
<div class="value">0%</div>
</div>
<!-- CSS -->
.circular-progress {
position: relative;
width: 150px;
height: 150px;
}
.circular-progress svg { transform: rotate(-90deg); }
.bg { fill: none; stroke: #1a1a2e; stroke-width: 10; }
.progress {
fill: none;
stroke: url(#gradient);
stroke-width: 10;
stroke-linecap: round;
stroke-dasharray: 408; /* 2 * PI * 65 */
stroke-dashoffset: 408;
transition: stroke-dashoffset 2s ease-out;
}
.value {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 2rem; font-weight: 800;
}
<!-- JavaScript -->
function animateCircular(element) {
const value = parseInt(element.dataset.value);
const circle = element.querySelector('.progress');
const valueEl = element.querySelector('.value');
const circumference = 2 * Math.PI * 65; // ~408
const offset = circumference - (value / 100) * circumference;
circle.style.strokeDashoffset = offset;
// Animate the number
let current = 0;
const interval = setInterval(() => {
current++;
valueEl.textContent = current + '%';
if (current >= value) clearInterval(interval);
}, 2000 / value);
}
3. Animated bar chart
A simple but effective bar chart. Each bar animates from height 0 to its final value, creating a satisfying "growth" effect.
<!-- HTML -->
<div class="bar-chart">
<div class="bar-item">
<div class="bar-value">85%</div>
<div class="bar" data-value="85"></div>
<div class="bar-label">Lun</div>
</div>
<!-- Repeat for each bar -->
</div>
<!-- CSS -->
.bar-chart {
display: flex;
align-items: flex-end;
gap: 16px;
height: 200px;
padding: 20px;
}
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.bar {
width: 50px;
background: linear-gradient(180deg, #6366f1, #8b5cf6);
border-radius: 8px 8px 0 0;
transition: height 1s ease-out;
}
<!-- JavaScript -->
function animateBars() {
const bars = document.querySelectorAll('.bar');
bars.forEach((bar, index) => {
const value = bar.dataset.value;
setTimeout(() => {
bar.style.height = (value / 100) * 150 + 'px';
}, index * 100); // Stagger effect
});
}
4. Statistics cards with animation
Statistics cards combine multiple elements: an icon, an animated counter and a label. The staggered entry effect creates a pleasant visual cascade.
<!-- HTML -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-value" data-target="15420">0</div>
<div class="stat-label">Utilisateurs</div>
</div>
</div>
<!-- CSS -->
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.stat-card {
background: #12121a;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 24px;
text-align: center;
opacity: 0;
transform: translateY(20px);
transition: all 0.6s ease-out;
}
.stat-card.visible {
opacity: 1;
transform: translateY(0);
}
.stat-value {
font-size: 2.5rem;
font-weight: 900;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
<!-- JavaScript -->
function animateStats() {
const cards = document.querySelectorAll('.stat-card');
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('visible');
const valueEl = card.querySelector('.stat-value');
animateCounter(valueEl);
}, index * 200);
});
}
5. Animated Gauge
The gauge is ideal for displaying scores, performance or levels. The SVG arc fills progressively while the needle pivots towards the target value.
<!-- HTML -->
<div class="gauge" data-value="72">
<svg width="200" height="120" viewBox="0 0 200 120">
<defs>
<linearGradient id="gaugeGradient">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="50%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#22c55e"/>
</linearGradient>
</defs>
<path class="bg-arc" d="M 20 100 A 80 80 0 0 1 180 100"/>
<path class="progress-arc" d="M 20 100 A 80 80 0 0 1 180 100"/>
<circle class="needle" cx="100" cy="100" r="8"/>
<line class="needle" x1="100" y1="100" x2="100" y2="30"/>
</svg>
<div class="gauge-value">0</div>
</div>
<!-- CSS -->
.gauge { position: relative; width: 200px; height: 120px; }
.bg-arc { fill: none; stroke: #1a1a2e; stroke-width: 20; }
.progress-arc {
fill: none;
stroke: url(#gaugeGradient);
stroke-width: 20;
stroke-linecap: round;
stroke-dasharray: 251; /* Arc length */
stroke-dashoffset: 251;
transition: stroke-dashoffset 2s ease-out;
}
.needle {
transform-origin: 100px 100px;
transition: transform 2s ease-out;
}
<!-- JavaScript -->
function animateGauge(element) {
const value = parseInt(element.dataset.value);
const arc = element.querySelector('.progress-arc');
const needles = element.querySelectorAll('.needle');
const valueEl = element.querySelector('.gauge-value');
const arcLength = 251;
const offset = arcLength - (value / 100) * arcLength;
const rotation = -90 + (value / 100) * 180;
arc.style.strokeDashoffset = offset;
needles.forEach(n => n.style.transform = `rotate(${rotation}deg)`);
// Animate number
let current = 0;
const interval = setInterval(() => {
current++;
valueEl.textContent = current;
if (current >= value) clearInterval(interval);
}, 2000 / value);
}
Best practices
Performance
- Use
requestAnimationFramerather thansetIntervalfor smooth animations - Prefer animatable CSS properties like
transformandopacity - Limit the number of simultaneous animations to avoid slowdowns
- Use
will-changesparingly on animated elements
UX and Design
- Animation duration: 1-2 seconds for data, no more
- Use natural easings like
ease-outor cubic curves - Trigger on scroll to avoid invisible animations on load
- Stagger the animations for a pleasant cascade effect
Accessibility
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
.counter-value,
.circular-progress .progress,
.bar,
.stat-card,
.gauge .progress-arc,
.gauge .needle {
transition: none;
animation: none;
}
}
/* Add ARIA attributes */
<div class="circular-progress"
role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Progress: 75%">
</div>
Too many animations can distract the user and harm readability. Reserve these effects for important data and key moments in the user journey.
Conclusion
Animating data is an excellent way to make your interfaces more engaging and memorable. The 5 techniques presented here cover the majority of use cases: counters for key figures, progress bars for percentages, charts for comparisons, cards for KPIs, and gauges for scores.
Don't forget to always respect your users' accessibility preferences and measure the impact of these animations on performance. Used sparingly, they will transform your dashboards and statistics pages.
Find over 50 data visualization effects in our effects library, with one-click copyable code.