StealThis .dev

Progress Ring (Remotion)

Animated circular progress rings showing Lighthouse-style performance scores with spring animations — rendered with Remotion at 1280×720 30fps.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

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

const RINGS = [
  { label: "Performance", value: 92, color: "#6366f1", delay: 10 },
  { label: "Accessibility", value: 88, color: "#10b981", delay: 20 },
  { label: "Best Practices", value: 95, color: "#f59e0b", delay: 30 },
  { label: "SEO", value: 100, color: "#ef4444", delay: 40 },
];

const RING_SIZE = 130;
const STROKE_WIDTH = 10;

const ProgressRing: React.FC<{ ring: (typeof RINGS)[number]; frame: number; fps: number }> = ({
  ring,
  frame,
  fps,
}) => {
  const f = Math.max(0, frame - ring.delay);
  const progress = spring({
    frame: f,
    fps,
    from: 0,
    to: ring.value / 100,
    config: { damping: 20, stiffness: 50 },
  });
  const opacity = interpolate(f, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const scale = spring({
    frame: f,
    fps,
    from: 0.7,
    to: 1,
    config: { damping: 12, stiffness: 100 },
  });

  const radius = (RING_SIZE - STROKE_WIDTH) / 2;
  const circumference = 2 * Math.PI * radius;
  const strokeDashoffset = circumference * (1 - progress);

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

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: 16,
        opacity,
        transform: `scale(${scale})`,
      }}
    >
      <div style={{ position: "relative", width: RING_SIZE, height: RING_SIZE }}>
        <svg width={RING_SIZE} height={RING_SIZE} style={{ transform: "rotate(-90deg)" }}>
          {/* Background ring */}
          <circle
            cx={RING_SIZE / 2}
            cy={RING_SIZE / 2}
            r={radius}
            fill="none"
            stroke="rgba(255,255,255,0.06)"
            strokeWidth={STROKE_WIDTH}
          />
          {/* Progress ring */}
          <circle
            cx={RING_SIZE / 2}
            cy={RING_SIZE / 2}
            r={radius}
            fill="none"
            stroke={ring.color}
            strokeWidth={STROKE_WIDTH}
            strokeLinecap="round"
            strokeDasharray={circumference}
            strokeDashoffset={strokeDashoffset}
            style={{ filter: `drop-shadow(0 0 8px ${ring.color}60)` }}
          />
        </svg>
        {/* Value text */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          <span
            style={{
              fontFamily: "system-ui, sans-serif",
              fontWeight: 800,
              fontSize: 32,
              color: "#ffffff",
            }}
          >
            {displayValue}
          </span>
        </div>
      </div>
      <span
        style={{
          fontFamily: "system-ui, sans-serif",
          fontWeight: 500,
          fontSize: 14,
          color: "rgba(255,255,255,0.6)",
          textAlign: "center",
        }}
      >
        {ring.label}
      </span>
    </div>
  );
};

export const ProgressRingVideo: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const titleOpacity = interpolate(frame, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const titleY = spring({ frame, fps, from: -20, to: 0, config: { damping: 14, stiffness: 80 } });

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

  const glowOpacity = interpolate(frame, [0, 30], [0, 0.2], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  return (
    <AbsoluteFill style={{ backgroundColor: "#0a0a0f" }}>
      {/* Background glow */}
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          width: 1000,
          height: 400,
          borderRadius: "50%",
          background: "radial-gradient(ellipse, rgba(99,102,241,0.15) 0%, transparent 70%)",
          transform: "translate(-50%, -50%)",
          opacity: glowOpacity,
        }}
      />

      {/* Title */}
      <div style={{ position: "absolute", top: 80, left: 0, right: 0, textAlign: "center" }}>
        <div style={{ opacity: titleOpacity, transform: `translateY(${titleY}px)` }}>
          <span
            style={{
              fontFamily: "system-ui, sans-serif",
              fontWeight: 800,
              fontSize: 42,
              color: "#ffffff",
              letterSpacing: -1,
            }}
          >
            Lighthouse Score
          </span>
        </div>
        <div style={{ opacity: subtitleOpacity, marginTop: 8 }}>
          <span
            style={{
              fontFamily: "system-ui, sans-serif",
              fontWeight: 400,
              fontSize: 18,
              color: "rgba(255,255,255,0.4)",
            }}
          >
            stealthis.dev — Performance Audit
          </span>
        </div>
      </div>

      {/* Rings */}
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -30%)",
          display: "flex",
          gap: 48,
          alignItems: "flex-start",
        }}
      >
        {RINGS.map((ring) => (
          <ProgressRing key={ring.label} ring={ring} frame={frame} fps={fps} />
        ))}
      </div>

      {/* Bottom badge */}
      <div
        style={{
          position: "absolute",
          bottom: 50,
          left: "50%",
          transform: "translateX(-50%)",
          opacity: interpolate(frame, [60, 80], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          }),
        }}
      >
        <div
          style={{
            padding: "8px 24px",
            borderRadius: 20,
            backgroundColor: "rgba(99,102,241,0.15)",
            border: "1px solid rgba(99,102,241,0.3)",
          }}
        >
          <span
            style={{
              fontFamily: "system-ui, sans-serif",
              fontWeight: 600,
              fontSize: 14,
              color: "#6366f1",
            }}
          >
            All scores above 85 — Great job!
          </span>
        </div>
      </div>
    </AbsoluteFill>
  );
};

export const RemotionRoot: React.FC = () => (
  <Composition
    id="ProgressRingVideo"
    component={ProgressRingVideo}
    durationInFrames={150}
    fps={30}
    width={1280}
    height={720}
  />
);

Four animated SVG progress rings inspired by Lighthouse audit scores, with staggered spring-driven fill animations and a summary badge. Customize the RINGS array with your own labels, values, and colors.