StealThis .dev
Remotion Medium

Remotion — Survey Results Video

An animated 7-second Remotion composition presenting six product-satisfaction survey questions as staggered horizontal bars on a dark background, each row revealing a spring-driven fill, a live count-up percentage badge at the bar tip, and a respondent tally — finishing with a gold highlight pulse on the top-scoring question.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

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

// ── Palette & Config ──────────────────────────────────────────────────────────
const BG_COLOR = "#0a0a0f";

const ACCENT_COLORS = [
  "#6366f1", // indigo
  "#06b6d4", // cyan
  "#10b981", // emerald
  "#f59e0b", // amber
  "#8b5cf6", // violet
  "#ef4444", // rose
];

const GOLD = "#f59e0b";
const GOLD_GLOW = "rgba(245,158,11,0.35)";

// ── Data ──────────────────────────────────────────────────────────────────────
interface SurveyQuestion {
  icon: string;       // unicode symbol
  category: string;   // short category label
  question: string;   // question text
  answer: string;     // winning answer
  pct: number;        // percentage (0–100)
  respondents: number;
}

const SURVEY_TITLE = "Product Satisfaction Survey — Q3 2024";
const SURVEY_SUBTITLE = "2,400 respondents · Conducted Aug–Sep 2024";

const QUESTIONS: SurveyQuestion[] = [
  {
    icon: "★",
    category: "Overall",
    question: "How satisfied are you overall?",
    answer: "Very Satisfied",
    pct: 82,
    respondents: 1968,
  },
  {
    icon: "⚡",
    category: "Performance",
    question: "Does the app meet your speed expectations?",
    answer: "Yes, always",
    pct: 74,
    respondents: 1776,
  },
  {
    icon: "◈",
    category: "Design",
    question: "How intuitive is the user interface?",
    answer: "Very intuitive",
    pct: 68,
    respondents: 1632,
  },
  {
    icon: "♻",
    category: "Support",
    question: "Was our support team helpful?",
    answer: "Extremely helpful",
    pct: 91,
    respondents: 2184,
  },
  {
    icon: "✦",
    category: "Value",
    question: "Do you consider the pricing fair?",
    answer: "Definitely fair",
    pct: 63,
    respondents: 1512,
  },
  {
    icon: "→",
    category: "Loyalty",
    question: "Would you recommend us to a friend?",
    answer: "Absolutely",
    pct: 87,
    respondents: 2088,
  },
];

// Row reveal: each row starts animating STAGGER frames after the previous
const STAGGER = 25;
// How long each bar spring takes to settle
const BAR_SPRING_DURATION = 40;
// When the header fades in
const HEADER_IN = 0;
// Gold highlight pulse starts after all bars appear
const HIGHLIGHT_START = STAGGER * QUESTIONS.length + BAR_SPRING_DURATION;

// Index of the question with the highest pct
const BEST_IDX = QUESTIONS.reduce(
  (best, q, i) => (q.pct > QUESTIONS[best].pct ? i : best),
  0
);

// ── Helpers ───────────────────────────────────────────────────────────────────
function useSpringValue(frame: number, startFrame: number, fps: number): number {
  return spring({
    frame: Math.max(0, frame - startFrame),
    fps,
    config: { damping: 18, stiffness: 120, mass: 1 },
  });
}

function lerp(a: number, b: number, t: number): number {
  return a + (b - a) * t;
}

// ── Sub-components ────────────────────────────────────────────────────────────

interface RowProps {
  question: SurveyQuestion;
  index: number;
  color: string;
  isHighlighted: boolean;
  highlightProgress: number;
  frame: number;
  fps: number;
}

const SurveyRow: React.FC<RowProps> = ({
  question,
  index,
  color,
  isHighlighted,
  highlightProgress,
  frame,
  fps,
}) => {
  const startFrame = STAGGER * index;

  // Row entrance: slide in from left + fade
  const rowEntrance = spring({
    frame: Math.max(0, frame - startFrame),
    fps,
    config: { damping: 20, stiffness: 100, mass: 1 },
  });

  // Bar fill progress
  const barProgress = useSpringValue(frame, startFrame + 5, fps);

  // Percentage counter (integer count-up)
  const displayPct = Math.round(lerp(0, question.pct, barProgress));

  // Respondents count-up
  const displayRespondents = Math.round(lerp(0, question.respondents, barProgress));

  const rowOpacity = interpolate(rowEntrance, [0, 1], [0, 1]);
  const rowX = interpolate(rowEntrance, [0, 1], [-40, 0]);

  // Gold highlight pulse (sinusoidal glow)
  const pulseAlpha = isHighlighted
    ? interpolate(highlightProgress, [0, 1], [0, 1], { extrapolateRight: "clamp" }) *
      (0.5 + 0.5 * Math.sin(highlightProgress * Math.PI * 6))
    : 0;

  // Bar max width in px (out of 1280 total, with left panel ~380px, right gutter 60px)
  const BAR_MAX_W = 720;
  const barWidth = BAR_MAX_W * (question.pct / 100) * barProgress;

  const rowHeight = 88;
  const barH = 28;

  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        height: rowHeight,
        opacity: rowOpacity,
        transform: `translateX(${rowX}px)`,
        borderRadius: 12,
        padding: "0 16px",
        background: isHighlighted
          ? `rgba(245,158,11,${0.06 * pulseAlpha + 0.04})`
          : "rgba(255,255,255,0.03)",
        boxShadow: isHighlighted
          ? `0 0 ${28 * pulseAlpha}px ${GOLD_GLOW}`
          : "none",
        border: isHighlighted
          ? `1px solid rgba(245,158,11,${0.4 * pulseAlpha + 0.1})`
          : "1px solid rgba(255,255,255,0.05)",
        marginBottom: 8,
        position: "relative",
        overflow: "hidden",
      }}
    >
      {/* Icon + Category */}
      <div
        style={{
          width: 56,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 2,
          flexShrink: 0,
        }}
      >
        <span
          style={{
            fontSize: 22,
            color: isHighlighted ? GOLD : color,
            filter: `drop-shadow(0 0 6px ${isHighlighted ? GOLD_GLOW : color}88)`,
          }}
        >
          {question.icon}
        </span>
        <span
          style={{
            fontSize: 9,
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 600,
            color: isHighlighted ? GOLD : color,
            letterSpacing: "0.08em",
            textTransform: "uppercase",
          }}
        >
          {question.category}
        </span>
      </div>

      {/* Question + Bar area */}
      <div style={{ flex: 1, paddingLeft: 16, paddingRight: 8 }}>
        {/* Question text */}
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontSize: 13,
            fontWeight: 500,
            color: "rgba(255,255,255,0.55)",
            marginBottom: 6,
            whiteSpace: "nowrap",
            overflow: "hidden",
            textOverflow: "ellipsis",
          }}
        >
          {question.question}
        </div>

        {/* Answer label */}
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontSize: 15,
            fontWeight: 700,
            color: isHighlighted ? GOLD : "rgba(255,255,255,0.90)",
            marginBottom: 8,
          }}
        >
          {question.answer}
        </div>

        {/* Bar */}
        <div
          style={{
            position: "relative",
            width: "100%",
            height: barH,
            background: "rgba(255,255,255,0.06)",
            borderRadius: barH / 2,
            overflow: "visible",
          }}
        >
          {/* Fill */}
          <div
            style={{
              position: "absolute",
              left: 0,
              top: 0,
              height: barH,
              width: barWidth,
              borderRadius: barH / 2,
              background: isHighlighted
                ? `linear-gradient(90deg, ${GOLD}cc, ${GOLD})`
                : `linear-gradient(90deg, ${color}aa, ${color})`,
              boxShadow: `0 0 16px ${isHighlighted ? GOLD_GLOW : color + "66"}`,
            }}
          />

          {/* Percentage badge at bar tip */}
          {barProgress > 0.05 && (
            <div
              style={{
                position: "absolute",
                top: "50%",
                left: barWidth + 8,
                transform: "translateY(-50%)",
                fontFamily: "system-ui, -apple-system, sans-serif",
                fontSize: 16,
                fontWeight: 700,
                color: isHighlighted ? GOLD : color,
                filter: `drop-shadow(0 0 4px ${isHighlighted ? GOLD_GLOW : color + "99"})`,
                whiteSpace: "nowrap",
              }}
            >
              {displayPct}%
            </div>
          )}
        </div>
      </div>

      {/* n= respondents */}
      <div
        style={{
          width: 90,
          flexShrink: 0,
          textAlign: "right",
          paddingRight: 4,
        }}
      >
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontSize: 11,
            fontWeight: 500,
            color: "rgba(255,255,255,0.30)",
            letterSpacing: "0.04em",
          }}
        >
          n =
        </div>
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontSize: 17,
            fontWeight: 700,
            color: isHighlighted ? `rgba(245,158,11,0.85)` : "rgba(255,255,255,0.55)",
          }}
        >
          {displayRespondents.toLocaleString()}
        </div>
      </div>
    </div>
  );
};

// ── Main Composition ──────────────────────────────────────────────────────────
export const SurveyResults: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Header entrance
  const headerSpring = spring({
    frame: Math.max(0, frame - HEADER_IN),
    fps,
    config: { damping: 22, stiffness: 90, mass: 1 },
  });
  const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
  const headerY = interpolate(headerSpring, [0, 1], [-24, 0]);

  // Footer/divider line grows in
  const lineProgress = interpolate(frame, [STAGGER * 0.5, STAGGER * 2], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Gold highlight progress for the best question
  const highlightProgress = interpolate(
    frame,
    [HIGHLIGHT_START, HIGHLIGHT_START + 60],
    [0, 1],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );

  return (
    <AbsoluteFill
      style={{
        background: BG_COLOR,
        fontFamily: "system-ui, -apple-system, sans-serif",
      }}
    >
      {/* Background radial glow */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(ellipse 900px 500px at 50% 40%, rgba(99,102,241,0.10) 0%, transparent 70%)",
          pointerEvents: "none",
        }}
      />
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(ellipse 600px 300px at 80% 70%, rgba(6,182,212,0.07) 0%, transparent 60%)",
          pointerEvents: "none",
        }}
      />

      {/* Content container */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          flexDirection: "column",
          padding: "40px 60px 36px",
        }}
      >
        {/* Header */}
        <div
          style={{
            opacity: headerOpacity,
            transform: `translateY(${headerY}px)`,
            marginBottom: 10,
          }}
        >
          <div
            style={{
              display: "flex",
              alignItems: "baseline",
              gap: 16,
              marginBottom: 4,
            }}
          >
            {/* Survey badge */}
            <div
              style={{
                background: "rgba(99,102,241,0.18)",
                border: "1px solid rgba(99,102,241,0.40)",
                borderRadius: 6,
                padding: "3px 10px",
                fontSize: 11,
                fontWeight: 700,
                color: "#6366f1",
                letterSpacing: "0.10em",
                textTransform: "uppercase",
              }}
            >
              Survey
            </div>
            <div
              style={{
                fontSize: 11,
                fontWeight: 500,
                color: "rgba(255,255,255,0.28)",
                letterSpacing: "0.06em",
              }}
            >
              CONFIDENTIAL · INTERNAL USE ONLY
            </div>
          </div>
          <div
            style={{
              fontSize: 28,
              fontWeight: 700,
              color: "rgba(255,255,255,0.95)",
              letterSpacing: "-0.02em",
              lineHeight: 1.1,
              marginBottom: 5,
            }}
          >
            {SURVEY_TITLE}
          </div>
          <div
            style={{
              fontSize: 14,
              fontWeight: 500,
              color: "rgba(255,255,255,0.40)",
              letterSpacing: "0.02em",
            }}
          >
            {SURVEY_SUBTITLE}
          </div>
        </div>

        {/* Divider line */}
        <div
          style={{
            height: 1,
            background: `linear-gradient(90deg, rgba(99,102,241,0.60), rgba(6,182,212,0.40), transparent)`,
            width: `${lineProgress * 100}%`,
            marginBottom: 18,
            borderRadius: 1,
          }}
        />

        {/* Rows */}
        <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
          {QUESTIONS.map((q, i) => (
            <SurveyRow
              key={q.category}
              question={q}
              index={i}
              color={ACCENT_COLORS[i % ACCENT_COLORS.length]}
              isHighlighted={i === BEST_IDX}
              highlightProgress={highlightProgress}
              frame={frame}
              fps={fps}
            />
          ))}
        </div>

        {/* Footer */}
        <div
          style={{
            opacity: interpolate(frame, [30, 55], [0, 1], {
              extrapolateLeft: "clamp",
              extrapolateRight: "clamp",
            }),
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            marginTop: 10,
            paddingTop: 10,
            borderTop: "1px solid rgba(255,255,255,0.06)",
          }}
        >
          <div
            style={{
              fontSize: 11,
              fontWeight: 500,
              color: "rgba(255,255,255,0.20)",
              letterSpacing: "0.05em",
            }}
          >
            stealthis.dev · Automated Report · 2024
          </div>
          <div
            style={{
              display: "flex",
              gap: 16,
              alignItems: "center",
            }}
          >
            {ACCENT_COLORS.map((c) => (
              <div
                key={c}
                style={{
                  width: 8,
                  height: 8,
                  borderRadius: "50%",
                  background: c,
                  opacity: 0.7,
                }}
              />
            ))}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

// ── Remotion Root ─────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
  <Composition
    id="SurveyResults"
    component={SurveyResults}
    durationInFrames={210}
    fps={30}
    width={1280}
    height={720}
  />
);

Survey Results Video

Six product-satisfaction questions — spanning Overall, Performance, Design, Support, Value, and Loyalty categories — are laid out as a vertically stacked dashboard on a deep #0a0a0f background. Each row contains a category icon and label on the left, a question and winning answer in the center, a horizontally filling bar driven by spring() physics, and an n = respondent count on the right. Rows reveal sequentially with a 25-frame stagger: the row fades and slides in from the left, then the bar fill begins a few frames later so the two motions layer visually without competing.

The percentage label sits just beyond the advancing bar tip and counts up as an integer derived from the spring progress value, giving the impression that the number is “following” the bar across the screen. A header line — survey title, subtitle, a badge chip, and a gradient divider that sweeps in left-to-right — sets context before the data appears. After all six bars have settled, the highest-scoring row (Support, 91 %) receives a gold highlight pulse: background tint, border glow, bar gradient, icon color, and percentage label all shift to amber and oscillate with a sinusoidal alpha so the pulse breathes rather than snapping.

The color palette assigns one of six distinct hues (indigo, cyan, emerald, amber, violet, rose) to each row, with individual colored glow shadows on bar fills. A subtle radial indigo glow at the top center and a secondary cyan glow at bottom-right give the composition spatial depth without competing with the data. Typography is system-ui at three weight levels: 700 for values, 600/500 for labels, and muted rgba(255,255,255,0.55) for secondary copy.

Composition specs

PropertyValue
Resolution1280 × 720
FPS30
Duration7 s (210 frames)

Data format

All survey data is hardcoded in the QUESTIONS constant at the top of the file. Each entry implements the SurveyQuestion interface:

interface SurveyQuestion {
  icon: string;        // unicode glyph shown before the category label
  category: string;    // short label (e.g. "Support")
  question: string;    // full question text
  answer: string;      // winning answer option
  pct: number;         // percentage of respondents who chose this answer (0–100)
  respondents: number; // absolute count of respondents who chose this answer
}

To customize the composition, replace the entries in QUESTIONS, adjust SURVEY_TITLE and SURVEY_SUBTITLE, and tweak STAGGER (frames between row entrances) or the spring() config inside SurveyRow for faster or slower bar fills. BEST_IDX is computed automatically as the index of the entry with the highest pct, so the gold highlight always targets the correct row.