remotion infographics data-visualization animation charts

Animated Infographics with Remotion: Data Storytelling in Motion

Animated Infographics with Remotion: Data Storytelling in Motion

Static infographics are easy to scroll past. Animated ones demand attention — a number counting up to 84% triggers a different cognitive response than simply reading “84%”. A bar growing from zero to its final height tells a story of growth rather than just stating a quantity. This is not an aesthetic preference; it is how the human visual system works. Motion implies change, and change implies meaning.

Remotion is unusually well-suited for animated infographics because it treats animation as deterministic, frame-based computation rather than as event-driven side effects. Every animation can be derived from a single integer — the current frame number — which means you can scrub through your infographic backward and forward without any state management, debug specific frames with mathematical precision, and render at any speed without worrying about timing drift.

This guide covers the core building blocks for data-driven infographics in Remotion: number counters, animated bar charts, donut chart rings, line chart draw-on animations, data binding from JSON props, and staggered element entrances.


The Foundation: spring and interpolate

Nearly every infographic animation in Remotion reduces to two primitives: spring for physically-modeled easing and interpolate for linear mapping between value ranges.

spring

spring models a spring-mass-damper system. It accepts the current frame and configuration, and returns a value that starts at 0, overshoots toward 1, and settles at 1. The overshoot and settle time are controlled by stiffness, damping, and mass.

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

const frame = useCurrentFrame();
const { fps } = useVideoConfig();

const progress = spring({
  frame,
  fps,
  config: {
    stiffness: 80,    // Higher = snappier
    damping: 14,      // Higher = less overshoot
    mass: 1,
  },
});
// progress goes from 0 to ~1 with a natural bounce

For infographic data values: multiply progress by the target value, and you have a smooth animated approach from zero to that value.

interpolate

interpolate maps a value from one range to another, with optional clamping and easing functions.

import { interpolate } from "remotion";

// Map frame 0-60 to opacity 0-1, clamped
const opacity = interpolate(frame, [0, 60], [0, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});

The two tools complement each other: spring for data values that need to feel organic, interpolate for timing-based transitions that need precise control.


Number Counter

A number counting up to a target value is the most common infographic element.

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

interface CounterProps {
  targetValue: number;
  label: string;
  prefix?: string;
  suffix?: string;
  startFrame?: number;
}

export const AnimatedCounter: React.FC<CounterProps> = ({
  targetValue,
  label,
  prefix = "",
  suffix = "",
  startFrame = 0,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame: Math.max(0, frame - startFrame),
    fps,
    config: {
      stiffness: 60,
      damping: 12,
      mass: 1,
    },
  });

  const displayValue = Math.round(progress * targetValue);

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
      }}
    >
      <div
        style={{
          fontSize: 96,
          fontWeight: 800,
          color: "#1a1a2e",
          lineHeight: 1,
          letterSpacing: "-0.02em",
        }}
      >
        {prefix}{displayValue.toLocaleString()}{suffix}
      </div>
      <div
        style={{
          fontSize: 20,
          color: "#64748b",
          marginTop: 12,
          textTransform: "uppercase",
          letterSpacing: "0.08em",
        }}
      >
        {label}
      </div>
    </div>
  );
};

Using toLocaleString() ensures large numbers like 1,240,000 display with proper comma separators. The startFrame prop allows you to stagger multiple counters — the first counter starts at frame 0, the second at frame 20, the third at frame 40, creating a cascading entrance.

Formatting Considerations

For percentages, use suffix="%" and pass targetValue={84}. For currency, use prefix="$" and pass the raw number. For large abbreviated numbers (“12.4M”), format the value in the component rather than relying on toLocaleString:

const formatLargeNumber = (n: number): string => {
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
  return n.toString();
};

Animated Bar Chart

A horizontal bar chart where each bar grows from left to right, with bars staggering their entrance for a sequential storytelling effect.

import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

interface BarData {
  label: string;
  value: number;
  color: string;
}

interface BarChartProps {
  data: BarData[];
  maxValue: number;
  startFrame?: number;
  staggerFrames?: number;
}

export const AnimatedBarChart: React.FC<BarChartProps> = ({
  data,
  maxValue,
  startFrame = 0,
  staggerFrames = 8,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 24,
        width: "100%",
        fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
      }}
    >
      {data.map((bar, index) => {
        const barStartFrame = startFrame + index * staggerFrames;
        const progress = spring({
          frame: Math.max(0, frame - barStartFrame),
          fps,
          config: { stiffness: 70, damping: 16, mass: 1 },
        });

        const widthPercent = (bar.value / maxValue) * 100 * progress;

        const labelOpacity = interpolate(
          frame,
          [barStartFrame, barStartFrame + 10],
          [0, 1],
          { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
        );

        return (
          <div key={bar.label} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div
              style={{
                display: "flex",
                justifyContent: "space-between",
                opacity: labelOpacity,
              }}
            >
              <span style={{ fontSize: 16, color: "#334155", fontWeight: 600 }}>
                {bar.label}
              </span>
              <span style={{ fontSize: 16, color: bar.color, fontWeight: 700 }}>
                {bar.value.toLocaleString()}
              </span>
            </div>
            <div
              style={{
                height: 40,
                background: "#f1f5f9",
                borderRadius: 6,
                overflow: "hidden",
              }}
            >
              <div
                style={{
                  width: `${widthPercent}%`,
                  height: "100%",
                  background: bar.color,
                  borderRadius: 6,
                }}
              />
            </div>
          </div>
        );
      })}
    </div>
  );
};

Data from JSON Props

Remotion compositions accept inputProps, which can be any JSON-serializable data. This is the correct pattern for data-driven infographics:

// In your composition registration (Root.tsx)
<Composition
  id="BarChartInfographic"
  component={BarChartInfographic}
  width={1920}
  height={1080}
  fps={30}
  durationInFrames={150}
  defaultProps={{
    data: [
      { label: "North America", value: 4200, color: "#3b82f6" },
      { label: "Europe", value: 3100, color: "#8b5cf6" },
      { label: "Asia Pacific", value: 5800, color: "#06b6d4" },
      { label: "Latin America", value: 1200, color: "#10b981" },
    ],
    maxValue: 6000,
  }}
/>

At render time, override the data via --props flag or the Node.js API’s inputProps parameter:

npx remotion render BarChartInfographic out.mp4 \
  --props '{"data":[{"label":"Q1","value":2400,"color":"#3b82f6"}],"maxValue":3000}'

This decoupling of animation code from data is what makes Remotion a practical tool for automated reporting pipelines.


Donut Chart Ring

A donut chart where the ring fills in from 0 to the target percentage, animated with SVG stroke-dashoffset.

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

interface DonutProps {
  percentage: number;
  label: string;
  color?: string;
  size?: number;
  strokeWidth?: number;
}

export const AnimatedDonut: React.FC<DonutProps> = ({
  percentage,
  label,
  color = "#3b82f6",
  size = 240,
  strokeWidth = 24,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const radius = (size - strokeWidth) / 2;
  const circumference = 2 * Math.PI * radius;

  const progress = spring({
    frame,
    fps,
    config: { stiffness: 50, damping: 14, mass: 1 },
  });

  const dashOffset = circumference - (circumference * percentage * progress) / 100;
  const displayPercentage = Math.round(percentage * progress);

  return (
    <div
      style={{
        position: "relative",
        width: size,
        height: size,
        fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
      }}
    >
      <svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
        {/* Track */}
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          fill="none"
          stroke="#e2e8f0"
          strokeWidth={strokeWidth}
        />
        {/* Fill */}
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          fill="none"
          stroke={color}
          strokeWidth={strokeWidth}
          strokeDasharray={circumference}
          strokeDashoffset={dashOffset}
          strokeLinecap="round"
        />
      </svg>
      {/* Center text */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <span
          style={{
            fontSize: size * 0.22,
            fontWeight: 800,
            color: "#1e293b",
            lineHeight: 1,
          }}
        >
          {displayPercentage}%
        </span>
        <span
          style={{
            fontSize: size * 0.09,
            color: "#64748b",
            marginTop: size * 0.03,
            textAlign: "center",
            maxWidth: radius * 1.2,
          }}
        >
          {label}
        </span>
      </div>
    </div>
  );
};

The SVG approach is preferable to CSS clip-path or conic-gradient for donut charts because SVG stroke-dashoffset interpolation is mathematically precise and renders identically in all browsers and in Remotion’s headless Chromium renderer.


Line Chart Draw-On Animation

A line chart where the path draws itself from left to right over time, using SVG path and stroke-dashoffset.

import { interpolate, useCurrentFrame, useVideoConfig } from "remotion";

interface LineChartProps {
  data: number[];
  width?: number;
  height?: number;
  color?: string;
  lineWidth?: number;
  animationDuration?: number; // frames
}

export const AnimatedLineChart: React.FC<LineChartProps> = ({
  data,
  width = 800,
  height = 400,
  color = "#3b82f6",
  lineWidth = 4,
  animationDuration = 90,
}) => {
  const frame = useCurrentFrame();

  const padding = { top: 20, right: 20, bottom: 40, left: 20 };
  const chartWidth = width - padding.left - padding.right;
  const chartHeight = height - padding.top - padding.bottom;

  const minValue = Math.min(...data);
  const maxValue = Math.max(...data);
  const valueRange = maxValue - minValue || 1;

  // Convert data to SVG coordinates
  const points = data.map((value, index) => ({
    x: padding.left + (index / (data.length - 1)) * chartWidth,
    y: padding.top + (1 - (value - minValue) / valueRange) * chartHeight,
  }));

  // Build SVG path
  const pathD = points
    .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`)
    .join(" ");

  // Compute total path length for stroke-dasharray animation
  // Approximated as sum of segment lengths
  const pathLength = points.reduce((total, point, i) => {
    if (i === 0) return total;
    const prev = points[i - 1];
    const dx = point.x - prev.x;
    const dy = point.y - prev.y;
    return total + Math.sqrt(dx * dx + dy * dy);
  }, 0);

  const drawProgress = interpolate(
    frame,
    [0, animationDuration],
    [0, 1],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );

  const dashOffset = pathLength * (1 - drawProgress);

  // Dot that traces along the line
  const dotIndex = Math.min(
    Math.floor(drawProgress * (points.length - 1)),
    points.length - 1
  );
  const dotPoint = points[dotIndex];

  return (
    <svg
      width={width}
      height={height}
      style={{ fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif' }}
    >
      {/* Grid lines */}
      {[0, 0.25, 0.5, 0.75, 1].map((fraction) => {
        const y = padding.top + fraction * chartHeight;
        return (
          <line
            key={fraction}
            x1={padding.left}
            y1={y}
            x2={padding.left + chartWidth}
            y2={y}
            stroke="#e2e8f0"
            strokeWidth={1}
          />
        );
      })}

      {/* Animated line */}
      <path
        d={pathD}
        fill="none"
        stroke={color}
        strokeWidth={lineWidth}
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeDasharray={pathLength}
        strokeDashoffset={dashOffset}
      />

      {/* Tracing dot */}
      {drawProgress > 0 && (
        <circle
          cx={dotPoint.x}
          cy={dotPoint.y}
          r={lineWidth * 2}
          fill={color}
        />
      )}
    </svg>
  );
};

This pattern — computing path length, setting strokeDasharray to the total length, and animating strokeDashoffset from pathLength to 0 — is the standard SVG draw-on technique. It works for any SVG <path> and generalizes to arrows, borders, and custom shapes.


Staggered Element Entrance

The NYT and FT data journalism pattern of revealing chart elements sequentially creates a narrative arc: the reader understands each element before the next one appears. In Remotion, this is a pure function of startFrame offsets.

// Utility: creates a spring-based reveal from a given startFrame
const useReveal = (startFrame: number, config = { stiffness: 80, damping: 14, mass: 1 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame: Math.max(0, frame - startFrame),
    fps,
    config,
  });

  return {
    opacity: Math.min(1, progress * 2), // Fast fade in
    translateY: (1 - progress) * 30,    // Slide up 30px
  };
};

// A full infographic scene
export const InfographicScene: React.FC<{ data: BarData[] }> = ({ data }) => {
  const title = useReveal(0);
  const subtitle = useReveal(15);
  const chart = useReveal(30);
  const footnote = useReveal(90 + data.length * 8);

  return (
    <div style={{ width: 1920, height: 1080, background: "#ffffff", padding: 80 }}>
      {/* Title */}
      <div
        style={{
          opacity: title.opacity,
          transform: `translateY(${title.translateY}px)`,
          fontSize: 56,
          fontWeight: 800,
          color: "#0f172a",
          marginBottom: 16,
          fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
        }}
      >
        Regional Revenue by Quarter
      </div>

      {/* Subtitle */}
      <div
        style={{
          opacity: subtitle.opacity,
          transform: `translateY(${subtitle.translateY}px)`,
          fontSize: 24,
          color: "#64748b",
          marginBottom: 60,
          fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
        }}
      >
        Full Year 2025 — All figures in USD thousands
      </div>

      {/* Chart */}
      <div
        style={{
          opacity: chart.opacity,
          transform: `translateY(${chart.translateY}px)`,
        }}
      >
        <AnimatedBarChart
          data={data}
          maxValue={Math.max(...data.map((d) => d.value)) * 1.1}
          startFrame={30}
          staggerFrames={10}
        />
      </div>

      {/* Footnote */}
      <div
        style={{
          opacity: footnote.opacity,
          transform: `translateY(${footnote.translateY}px)`,
          fontSize: 16,
          color: "#94a3b8",
          marginTop: 40,
          fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
        }}
      >
        Source: Internal financial system. Fiscal year ending December 31, 2025.
      </div>
    </div>
  );
};

Composition Timing Guidelines for 16:9 Infographics

For a standard widescreen (1920×1080) infographic:

  • Title entrance: frames 0–30
  • Subtitle / data source: frames 15–45
  • Chart reveal begins: frame 30
  • Bars/lines stagger at 8–12 frames per element
  • Number callouts (counters) stagger at 10–15 frames per element after the chart is visible
  • Hold on final state: minimum 60 frames (2 seconds at 30fps) before any exit animation
  • Total composition for a 4-bar chart with title, chart, and callouts: 180–240 frames (6–8 seconds at 30fps)

Data-Journalism Layout Principles

The New York Times, The Economist, and Financial Times have established visual conventions for data animation that are worth following:

One message per screen. Each scene of the infographic should communicate a single data point or trend. If you have five charts, that is five scenes, not one crowded slide.

Lead with the conclusion. The title should state the finding, not describe the chart. “Asia Pacific grew fastest in 2025” not “Regional revenue comparison.”

Animate toward the insight. If you are showing that one bar dwarfs the others, let the other bars appear first, then reveal the dominant bar last for maximum impact.

Restrained color palette. Use a single accent color for the highlight data series and neutral gray for everything else. One strong color draws the eye; five different colors create confusion.

Never animate axis labels. Labels should appear instantly, slightly before or simultaneously with the first data element. Animating labels is distracting and reduces readability.


RenderComp Data Visualization Templates

Building production-quality animated infographics from scratch requires significant front-end development time. RenderComp provides a library of Remotion-based infographic templates — number counters, bar charts, donut charts, and full-scene data story layouts — ready to customize with your data. Each template accepts JSON defaultProps so you can wire it into a reporting pipeline or render batch runs with different datasets.

Browse the template collection at rendercomp.com.


FAQ

Q: Can I use a real charting library like D3 or Recharts inside a Remotion composition? A: D3 works well inside Remotion because it is a low-level SVG manipulation library — you use it to compute coordinates, then render the SVG yourself. Recharts and other React chart libraries that rely on CSS transitions or ResizeObserver may not animate correctly in Remotion’s headless renderer. The recommended approach is to compute chart geometry manually (as shown in the examples above) or use D3 for coordinate math combined with React for rendering.

Q: How do I animate a number with decimal places, like a percentage showing 73.4%? A: Apply the spring or interpolate to the raw number and use toFixed() for display: (progress * targetValue).toFixed(1). Be aware that toFixed returns a string, so parseFloat it first if you need arithmetic.

Q: My bars are animating but they overshoot the container. How do I prevent this? A: Add overflow: 'hidden' to the bar track container. The spring value briefly exceeds 1 during the overshoot phase, which can make the filled portion temporarily exceed 100% of the container width. overflow: 'hidden' clips it cleanly.

Q: What resolution and aspect ratio should I use for infographics intended for social media? A: LinkedIn and Twitter/X feed posts render best at 1200×628 (1.91:1). Instagram square: 1080×1080. For embedded YouTube video essays or LinkedIn Articles: 1920×1080 (16:9). Define separate compositions for each target aspect ratio rather than trying to make one composition adapt.

Q: Is it possible to drive animations from a CSV file rather than JSON? A: Remotion’s inputProps must be JSON-serializable, so convert CSV to JSON in a preprocessing step before passing data to the composition. A simple Node.js script using the csv-parse library can do this in a few lines, and the resulting JSON can be passed directly to renderMedia().

Q: How do I add a data source / footnote that appears after all the chart animations complete? A: Use a startFrame equal to the frame after all chart animations have settled (typically the total stagger duration plus spring settle time of ~30 frames). Use interpolate with extrapolateRight: 'clamp' for a simple fade-in.

Q: Can I render the same infographic template with 12 different datasets automatically? A: Yes. Use the renderMedia() Node.js API in a loop, passing different inputProps for each dataset. Each call produces an independent output file. This is the core use case that makes Remotion attractive for data journalism workflows and automated business reporting.

Ready to ship

Get 1,400+ Remotion Templates

Lifetime license. TypeScript-first. Ship polished video in minutes, not days.

Get RenderComp →