Catalog

Edit Props (live)

JSON
TEXT
config
config
config

Source

167 lines
// Animated Bar Chart — bars grow in (spring, staggered) from a data array.
// Native Remotion + SVG-free flexbox, frame-driven. Data-viz is a category
// fixed-primitive tools can't easily template; this turns a JSON array of
// { label, value } into a clean animated chart.

import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { loadFont as loadGrotesk } from '@remotion/google-fonts/SpaceGrotesk';

const { fontFamily: GROTESK } = loadGrotesk();

type Datum = { label: string; value: number };

export type AnimatedBarChartProps = {
  data: Datum[] | string;
  title: string;
  barColor: string;
  textColor: string;
  backgroundColor: string;
};

function parseData(data: Datum[] | string): Datum[] {
  let raw: unknown = data;
  if (typeof data === 'string') {
    try {
      raw = JSON.parse(
        data.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '')
      );
    } catch {
      raw = [];
    }
  }
  if (!Array.isArray(raw)) return [];
  return raw.slice(0, 8).map((d) => ({
    label: String((d as Datum)?.label ?? ''),
    value: Number((d as Datum)?.value) || 0,
  }));
}

export default function AnimatedBarChart({
  data = [
    { label: 'Jan', value: 32 },
    { label: 'Feb', value: 54 },
    { label: 'Mar', value: 71 },
    { label: 'Apr', value: 48 },
    { label: 'May', value: 89 },
  ],
  title = '',
  barColor = '#5b3df5',
  textColor = '#ffffff',
  backgroundColor = '#0a0a14',
}: Partial<AnimatedBarChartProps>) {
  const frame = useCurrentFrame();
  const { fps, durationInFrames, width } = useVideoConfig();
  // Resolution-independent: px authored at a 1080-wide reference, scaled
  // by k. k=1 at 1080 (identical output), scales cleanly at any width.
  const k = width / 1080;
  const bars = parseData(data);
  const max = Math.max(1, ...bars.map((b) => b.value));

  const exit = interpolate(
    frame,
    [durationInFrames - 10, durationInFrames - 1],
    [1, 0],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );

  return (
    <AbsoluteFill
      style={{
        backgroundColor,
        fontFamily: GROTESK,
        opacity: exit,
        padding: `${120 * k}px ${80 * k}px ${100 * k}px`,
        boxSizing: 'border-box',
      }}
    >
      {title ? (
        <div
          style={{
            color: textColor,
            fontSize: 64 * k,
            fontWeight: 800,
            letterSpacing: '-0.02em',
            marginBottom: 48 * k,
            opacity: interpolate(frame, [0, 12], [0, 1], {
              extrapolateRight: 'clamp',
            }),
          }}
        >
          {title}
        </div>
      ) : null}

      <div
        style={{
          flex: 1,
          display: 'flex',
          alignItems: 'flex-end',
          justifyContent: 'space-between',
          gap: 28 * k,
        }}
      >
        {bars.map((b, i) => {
          const grow = spring({
            frame: frame - 8 - i * 4,
            fps,
            config: { damping: 15, stiffness: 90 },
            durationInFrames: 26,
          });
          const hPct = (b.value / max) * 100 * grow;
          return (
            <div
              key={i}
              style={{
                flex: 1,
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                height: '100%',
                justifyContent: 'flex-end',
              }}
            >
              <div
                style={{
                  color: textColor,
                  fontSize: 38 * k,
                  fontWeight: 700,
                  opacity: grow,
                  marginBottom: 12 * k,
                  fontVariantNumeric: 'tabular-nums',
                }}
              >
                {Math.round(b.value * grow)}
              </div>
              <div
                style={{
                  width: '100%',
                  height: `${hPct}%`,
                  background: barColor,
                  borderRadius: `${10 * k}px ${10 * k}px 0 0`,
                }}
              />
              <div
                style={{
                  color: textColor,
                  opacity: 0.7,
                  fontSize: 34 * k,
                  fontWeight: 500,
                  marginTop: 16 * k,
                }}
              >
                {b.label}
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
}

Schema (Zod)

import { z } from 'zod';

const datumSchema = z.object({
  label: z.string(),
  value: z.number(),
});

export const Schema = z.object({
  data: z
    .union([z.array(datumSchema), z.string()])
    .default([
      { label: 'Jan', value: 32 },
      { label: 'Feb', value: 54 },
      { label: 'Mar', value: 71 },
      { label: 'Apr', value: 48 },
      { label: 'May', value: 89 },
    ])
    .describe('$port:json Array of { label, value } (up to 8 bars)'),
  title: z.string().default('').describe('$port:text Chart title (optional)'),
  barColor: z.string().default('#5b3df5').describe('$style:color Bar color'),
  textColor: z.string().default('#ffffff').describe('$style:color Text color'),
  backgroundColor: z
    .string()
    .default('#0a0a14')
    .describe('$style:color Background color'),
});

Animated Bar Chart

animated_bar_chart

Bars grow in (spring, staggered) from a JSON array of { label, value }, each counting up to its value. Drop-in data-viz for any comparison, ranking, or trend.

Workflow Inputs (2)

  • Array of { label, value } (up to 8 bars)
    data
    JSON
  • Chart title (optional)
    title
    TEXT

Config Fields (3)

  • barColor
  • textColor
  • backgroundColor

Meta

Updated
7/10/2026