Animated Infographic

Gradients, animation, and a live counter combined

Combining three techniques

This infographic layers three SVG capabilities on top of each other. A <linearGradient> fills the chart bars with an orange-to-red sweep. CSS @keyframes animations grow those bars from zero height and draw a progress ring around a circular counter. A JavaScript requestAnimationFrame loop increments the number inside the ring, timing its count to match the ring draw animation. Together they make a self-contained, accessible SVG that communicates data through motion.

Defining the gradient

Gradients live in <defs> and are referenced by id. The two stop colors here are drawn from the on-palette range — amber (#f0a500) fading to red (#d6452c). Setting gradientUnits="objectBoundingBox" (the SVG default) means gradient coordinates are expressed as fractions of each shape's own bounding box — so x1="0" y1="0" x2="0" y2="1" always runs top-to-bottom across whatever shape the gradient fills, regardless of that shape's position or height.

<defs> <linearGradient id="infographic-bar-grad" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox"> <stop offset="0%" stop-color="#f0a500"/> <stop offset="100%" stop-color="#d6452c"/> </linearGradient> <!-- Teal gradient for the progress ring track fill --> <linearGradient id="infographic-ring-grad" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#1f8a4c"/> <stop offset="100%" stop-color="#0f766e"/> </linearGradient> </defs>

Apply the gradient with fill="url(#infographic-bar-grad)" — the same syntax that also works for stroke.

CSS animation: growing bars and the ring draw

Bars scale from zero height using transform: scaleY(0)scaleY(1) with transform-origin: center bottom pinned at the baseline. A staggered animation-delay creates a sequential reveal.

The progress ring uses the stroke-dashoffset trick: set stroke-dasharray equal to the circle's circumference (2πr), then animate stroke-dashoffset from the full circumference down to the offset representing the remaining percentage. The visible dash grows as the offset shrinks.

/* Bars: scale from baseline */ @keyframes infographic-bar-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } } /* Ring: dashoffset runs from full circumference down to target */ @keyframes infographic-ring-draw { from { stroke-dashoffset: var(--ring-circumference); } to { stroke-dashoffset: var(--ring-target-offset); } } .infographic-bar { transform-origin: center bottom; animation: infographic-bar-grow 0.9s cubic-bezier(0.34, 1.1, 0.64, 1) both; } .infographic-ring-progress { animation: infographic-ring-draw 1.2s cubic-bezier(0.4, 0, 0.2, 1) both; } /* Stagger each bar */ .infographic-bar:nth-child(1) { animation-delay: 0.05s; } .infographic-bar:nth-child(2) { animation-delay: 0.15s; } /* Disable everything for users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { .infographic-bar, .infographic-ring-progress { animation: none; } }

JavaScript: the counting animation

A <text> element in the SVG displays a number that counts up from 0 to 78 using requestAnimationFrame. The counter respects prefers-reduced-motion — if the preference is set, it skips straight to the final value without any looping.

function animateCounter(textEl, target, durationMs) { // Respect the user's motion preference — jump to final value immediately if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { textEl.textContent = target; return; } const start = performance.now(); function tick(now) { const elapsed = now - start; const progress = Math.min(elapsed / durationMs, 1); // Ease out: progress slows as it approaches 1 const eased = 1 - Math.pow(1 - progress, 3); textEl.textContent = Math.round(eased * target); if (progress < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); } animateCounter( document.getElementById('infographic-counter-text'), 78, // target value (78%) 1200 // duration matches the ring-draw animation );

Live demo

Load or replay the demo to watch the bars grow, the ring draw, and the counter count up. The Replay button restarts all animations. If you have "Reduce Motion" enabled in your OS accessibility settings, the counter jumps straight to the final value and the CSS animations are disabled.

Weekly activity bar chart Animated bar chart showing relative activity levels for each day of the week: Mon 60%, Tue 85%, Wed 55%, Thu 90%, Fri 78%. 50 75 100 Mon Tue Wed Thu Fri
Weekly activity
Goal completion progress ring Circular progress ring showing 78% goal completion. The ring animates from empty to 78% on load, and a counter counts up to 78 inside the ring. 0 % complete
Goal completion

Accessibility: making the infographic readable

Each SVG carries role="img" so assistive technology presents it as a single graphic. The first child of each SVG is a <title> (the accessible name), followed by a <desc> with a full prose summary of the data. A screen reader user gets the complete information without needing to navigate the individual shapes.

The prefers-reduced-motion media query appears in two places. In CSS it sets animation: none on every animated element. In JavaScript the counter function checks window.matchMedia('(prefers-reduced-motion: reduce)').matches before starting the requestAnimationFrame loop — if motion is reduced, it writes the final value directly. This dual guard is necessary because CSS cannot stop a JavaScript animation, and JavaScript cannot suppress CSS animations by itself.

The stroke-dashoffset technique

The progress ring relies on a classic SVG trick. A circle has a circumference of 2πr. Setting stroke-dasharray equal to the circumference creates a dash that exactly wraps the circle. Setting stroke-dashoffset to the full circumference shifts the dash entirely off the visible start of the stroke — making it invisible. Animating the offset back toward zero causes the dash to sweep visibly around the ring. Stopping at an offset that equals circumference × (1 - fraction) leaves exactly the desired percentage filled.

<!-- r = 80, circumference = 2 * π * 80 ≈ 502.65 --> <!-- For 78%: target-offset = 502.65 * (1 - 0.78) ≈ 110.58 --> <circle stroke-dasharray="502.65" stroke-dashoffset="502.65" <!-- fully hidden at start --> style=" --ring-circumference: 502.65; --ring-target-offset: 110.58; /* animation end value */ " class="infographic-ring-progress" />