TikTok-style captions in Remotion
Burn TikTok-style, word-by-word captions into a Remotion video — the @remotion/captions building blocks, a from-scratch page renderer, and drop-in components that take your Whisper or SRT transcript as-is.
Short answer: get a word-level transcript in milliseconds (Whisper gives you
one), group it into pages with createTikTokStyleCaptions() from
@remotion/captions, and render each page in a <Sequence> that highlights the
word being spoken. Or drop the transcript into a caption component and skip the
maths.
The shape everything speaks
Whisper, @remotion/captions and most caption tools produce the same thing: one
entry per word, timed in milliseconds.
import type { Caption } from "@remotion/captions";
const captions: Caption[] = [
{ text: "Stop", startMs: 0, endMs: 380, timestampMs: 190, confidence: null },
{ text: " losing", startMs: 380, endMs: 760, timestampMs: 570, confidence: null },
{ text: " hours", startMs: 760, endMs: 1200, timestampMs: 980, confidence: null },
];Note the leading space on every word after the first. The page builder splits on it; leave it out and the words run together.
Group into pages
TikTok captions show a few words at a time, not the whole sentence.
createTikTokStyleCaptions() groups words into pages; once a page runs longer than
combineTokensWithinMilliseconds, the next word starts a new one:
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: 1200,
});Each page has text, startMs, durationMs and tokens — the words, each with
fromMs and toMs.
Render a page, highlight the active word
import type { TikTokPage } from "@remotion/captions";
import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
const Page = ({ page }: { page: TikTokPage }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Inside a <Sequence>, frame 0 is the page's start.
const nowMs = page.startMs + (frame / fps) * 1000;
return (
<AbsoluteFill style={{ justifyContent: "flex-end", alignItems: "center", paddingBottom: "20%" }}>
<div style={{ fontSize: 72, fontWeight: 800, color: "white", whiteSpace: "pre" }}>
{page.tokens.map((t) => (
<span key={t.fromMs} style={{ color: nowMs >= t.fromMs && nowMs < t.toMs ? "#facc15" : "white" }}>
{t.text}
</span>
))}
</div>
</AbsoluteFill>
);
};
export const Captions = ({ pages }: { pages: TikTokPage[] }) => {
const { fps } = useVideoConfig();
return pages.map((page) => (
<Sequence
key={page.startMs}
from={Math.round((page.startMs / 1000) * fps)}
durationInFrames={Math.max(1, Math.round((page.durationMs / 1000) * fps))}
>
<Page page={page} />
</Sequence>
));
};whiteSpace: "pre" keeps the leading spaces. Put <Captions> in an
<AbsoluteFill> above your video and it overlays it.
Keep captions in the safe area: on 9:16 the bottom fifth is covered by the like, comment and share buttons, which is why this sits 20% up.
Or use a component
That renderer is the minimum. The look — stroke, pill, pop on the active word, paging on the speaker's pauses rather than a fixed word count — is where the time goes. snapcn's Word Captions takes the same transcript as-is:
npx shadcn@latest add @snapcn/word-captionsimport { WordCaptions } from "@/components/snap-cn/word-captions";
<WordCaptions captions={captions} preset="youtube" />;
// or paste an .srt file's contents
<WordCaptions srt={srtFileContents} preset="youtube" />;Milliseconds are converted against the composition's fps, so one transcript is
right at 24, 30 or 60fps. maxWords, maxChars and pageBreakMs override how it
pages.
For a whole line on screen with a fill sweeping across the spoken words, use Karaoke Captions:
import { KaraokeCaptions } from "@/components/snap-cn/karaoke-captions";
<KaraokeCaptions
lines={[
{
text: "Acme reconciles every transaction automatically",
startFrame: 10,
endFrame: 120,
wordTimings: [10, 28, 52, 68, 92],
emphasize: [4],
},
]}
aspect="portrait"
/>;Next: how to animate text in Remotion — the same rules apply to a caption that pops.
FAQ
How do you add TikTok-style captions in Remotion?
Transcribe the audio into captions with startMs and endMs per word, group them into pages with createTikTokStyleCaptions from @remotion/captions, and render each page in a Sequence that highlights the word being spoken.
What does createTikTokStyleCaptions do?
It takes an array of Caption objects and a combineTokensWithinMilliseconds value, and returns pages. Each page has text, startMs, durationMs and tokens with their own fromMs and toMs, so you can highlight the active word.
Why are there no spaces between my caption words?
createTikTokStyleCaptions uses the whitespace before each word as the delimiter, so every caption's text must start with a space except the first.
Can I use an SRT file for Remotion captions?
Yes. snapcn's Word Captions component takes an srt string directly and splits each cue into words, or a captions array in the same millisecond shape Whisper and @remotion/captions use.

