Blog / CSS/JS

Animate Your Data: Counters, Progress Bars and Charts

Discover how to bring your data to life with CSS and JavaScript animations. Counters, progress bars, charts and gauges: 5 essential techniques for dynamic interfaces.

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.

💡
Intersection Observer

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.

0
Active users
0
Client satisfaction
0
Downloads
counter.html
<!-- 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.

0%
0%
circular-progress.html
<!-- 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.

85%
Lun
65%
Mar
90%
Mer
45%
Jeu
70%
Ven
55%
Sam
30%
Dim
bar-chart.html
<!-- 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.

👥
0
Users
📈
0
Growth
0
Average rating
stats-cards.html
<!-- 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.

0
Performance Score
0
100
gauge.html
<!-- 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 requestAnimationFrame rather than setInterval for smooth animations
  • Prefer animatable CSS properties like transform and opacity
  • Limit the number of simultaneous animations to avoid slowdowns
  • Use will-change sparingly on animated elements

UX and Design

  • Animation duration: 1-2 seconds for data, no more
  • Use natural easings like ease-out or cubic curves
  • Trigger on scroll to avoid invisible animations on load
  • Stagger the animations for a pleasant cascade effect

Accessibility

accessibility.css
/* 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>
⚠️
Beware of excessive animations

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.

🎨
Go further

Find over 50 data visualization effects in our effects library, with one-click copyable code.