Caption Track

TikTok / Reels–style word-by-word captions for vertical video. Each word pops in over a background image or color.

Preview

Open editor

Usage

The signature look of viral vertical content. Each word in your text pops onto the screen one at a time — bold, oversized, all-caps, with a thick outline and drop shadow that reads cleanly over any background. Pace is controlled by wordsPerSecond (try 2.5 – 4 for natural rhythm; bump to 5+ for hype-energy edits).

Frame is 1080 × 1920 (9:16 vertical). Drop a backgroundImageUrl to overlay captions on a still, or leave it blank and use backgroundColor. Pair textColor (typically white or yellow) with a contrasting outlineColor (typically black) for max readability.

Props

NameTypeDefault
textstring"this is the future of motion graphics"
wordsPerSecondnumber3

Composition

ID
CaptionTrack
Resolution
1920×1080
FPS
60
Duration
2.7s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import { AbsoluteFill, spring, useVideoConfig } from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { useDesignFrame } from "../../use-design-frame";

export type CaptionTrackProps = {
  text: string;
  wordsPerSecond: number;
  clipStyle?: ClipStyle;
};

export const CaptionTrack: React.FC<CaptionTrackProps> = ({
  text,
  wordsPerSecond,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { fps } = useVideoConfig();
  const s = resolveClipStyle(clipStyle, {
    background: "#ffffff",
    color: "#0f1014",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: "#0a84ff",
  });

  const words = text.trim().split(/\s+/).filter(Boolean);
  const framesPerWord = Math.max(
    1,
    Math.round(fps / Math.max(0.5, wordsPerSecond)),
  );
  const startOffset = 8;

  const adjustedFrame = frame - startOffset;
  const wordIndex =
    adjustedFrame < 0
      ? -1
      : Math.min(words.length - 1, Math.floor(adjustedFrame / framesPerWord));
  const localFrame = adjustedFrame - wordIndex * framesPerWord;

  const wordPop = spring({
    frame: localFrame,
    fps,
    config: { damping: 12, stiffness: 220, mass: 0.5 },
  });

  const word = wordIndex >= 0 ? (words[wordIndex] ?? "") : "";

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        padding: "0 80px",
        fontFamily: s.fontFamily,
      }}
    >
      {word && (
        <span
          style={{
            fontSize: 132,
            fontWeight: 700,
            letterSpacing: "-0.045em",
            lineHeight: 1.05,
            color: s.color,
            textAlign: "center",
            transform: `scale(${0.7 + wordPop * 0.3})`,
            opacity: wordPop,
            display: "inline-block",
          }}
        >
          {word}
        </span>
      )}
    </AbsoluteFill>
  );
};
Save as CaptionTrack/CaptionTrack.tsx