Displaying captions
This guide explains how to display captions in Remotion, assuming you already have captions in the Caption format - see Transcribing audio for how to generate them.
Fetching captions
First, fetch your captions JSON file. Use useDelayRender() to hold the render until the captions are loaded:
import {useState , useEffect , useCallback } from 'react';
import {AbsoluteFill , staticFile , useDelayRender } from 'remotion';
import type {Caption } from '@remotion/captions';
export const MyComponent : React .FC = () => {
const [captions , setCaptions ] = useState <Caption [] | null>(null);
const {delayRender , continueRender , cancelRender } = useDelayRender ();
const [handle ] = useState (() => delayRender ());
const fetchCaptions = useCallback (async () => {
try {
const response = await fetch (staticFile ('captions.json'));
const data = await response .json ();
setCaptions (data );
continueRender (handle );
} catch (e ) {
cancelRender (e );
}
}, [continueRender , cancelRender , handle ]);
useEffect (() => {
fetchCaptions ();
}, [fetchCaptions ]);
if (!captions ) {
return null;
}
return <AbsoluteFill >{/* Render captions here */}</AbsoluteFill >;
};Creating pages
Use createTikTokStyleCaptions() to group captions into pages. The combineTokensWithinMilliseconds option controls how many words appear at once:
const {pages } = useMemo (() => {
return createTikTokStyleCaptions ({
captions ,
combineTokensWithinMilliseconds : SWITCH_CAPTIONS_EVERY_MS ,
});
}, [captions ]);Rendering with Sequences
Map over the pages and render each one in a <Sequence>. Calculate the start frame and duration from the page timing:
const CaptionedContent : React .FC = () => {
const {fps } = useVideoConfig ();
return (
<AbsoluteFill >
{pages .map ((page , index ) => {
const nextPage = pages [index + 1] ?? null;
const startFrame = (page .startMs / 1000) * fps ;
const endFrame = Math .min (nextPage ? (nextPage .startMs / 1000) * fps : Infinity , startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps );
const durationInFrames = endFrame - startFrame ;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence key ={index } from ={startFrame } durationInFrames ={durationInFrames }>
<CaptionPage page ={page } />
</Sequence >
);
})}
</AbsoluteFill >
);
};Rendering a caption page
A caption page contains tokens which you can use to highlight the currently spoken word. Here's an example that highlights words as they are spoken:
import {AbsoluteFill , useCurrentFrame , useVideoConfig } from 'remotion';
import type {TikTokPage } from '@remotion/captions';
const HIGHLIGHT_COLOR = '#39E508';
const CaptionPage : React .FC <{page : TikTokPage }> = ({page }) => {
const frame = useCurrentFrame ();
const {fps } = useVideoConfig ();
// Current time relative to the start of the sequence
const currentTimeMs = (frame / fps ) * 1000;
// Convert to absolute time by adding the page start
const absoluteTimeMs = page .startMs + currentTimeMs ;
return (
<AbsoluteFill
style ={{
justifyContent : 'center',
alignItems : 'center',
}}
>
<div
style ={{
fontSize : 80,
fontWeight : 'bold',
textAlign : 'center',
// Preserve whitespace in captions
whiteSpace : 'pre',
}}
>
{page .tokens .map ((token ) => {
const isActive = token .fromMs <= absoluteTimeMs && token .toMs > absoluteTimeMs ;
return (
<span
key ={token .fromMs }
style ={{
color : isActive ? HIGHLIGHT_COLOR : 'white',
}}
>
{token .text }
</span >
);
})}
</div >
</AbsoluteFill >
);
};Full example
Show full example
import {useState , useEffect , useCallback , useMemo } from 'react';
import {AbsoluteFill , Sequence , staticFile , useCurrentFrame , useDelayRender , useVideoConfig } from 'remotion';
import {createTikTokStyleCaptions } from '@remotion/captions';
import type {Caption , TikTokPage } from '@remotion/captions';
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const HIGHLIGHT_COLOR = '#39E508';
const CaptionPage : React .FC <{page : TikTokPage }> = ({page }) => {
const frame = useCurrentFrame ();
const {fps } = useVideoConfig ();
const currentTimeMs = (frame / fps ) * 1000;
const absoluteTimeMs = page .startMs + currentTimeMs ;
return (
<AbsoluteFill
style ={{
justifyContent : 'center',
alignItems : 'center',
}}
>
<div
style ={{
fontSize : 80,
fontWeight : 'bold',
textAlign : 'center',
whiteSpace : 'pre',
}}
>
{page .tokens .map ((token ) => {
const isActive = token .fromMs <= absoluteTimeMs && token .toMs > absoluteTimeMs ;
return (
<span
key ={token .fromMs }
style ={{
color : isActive ? HIGHLIGHT_COLOR : 'white',
}}
>
{token .text }
</span >
);
})}
</div >
</AbsoluteFill >
);
};
export const CaptionedVideo : React .FC = () => {
const [captions , setCaptions ] = useState <Caption [] | null>(null);
const {delayRender , continueRender , cancelRender } = useDelayRender ();
const [handle ] = useState (() => delayRender ());
const {fps } = useVideoConfig ();
const fetchCaptions = useCallback (async () => {
try {
const response = await fetch (staticFile ('captions.json'));
const data = await response .json ();
setCaptions (data );
continueRender (handle );
} catch (e ) {
cancelRender (e );
}
}, [continueRender , cancelRender , handle ]);
useEffect (() => {
fetchCaptions ();
}, [fetchCaptions ]);
const {pages } = useMemo (() => {
return createTikTokStyleCaptions ({
captions : captions ?? [],
combineTokensWithinMilliseconds : SWITCH_CAPTIONS_EVERY_MS ,
});
}, [captions ]);
return (
<AbsoluteFill style ={{backgroundColor : 'black'}}>
{pages .map ((page , index ) => {
const nextPage = pages [index + 1] ?? null;
const startFrame = (page .startMs / 1000) * fps ;
const endFrame = Math .min (nextPage ? (nextPage .startMs / 1000) * fps : Infinity , startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps );
const durationInFrames = endFrame - startFrame ;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence key ={index } from ={startFrame } durationInFrames ={durationInFrames }>
<CaptionPage page ={page } />
</Sequence >
);
})}
</AbsoluteFill >
);
};Next steps
You can customize the appearance of your captions:
- Use
fitText()from@remotion/layout-utilsto automatically scale text to fit the video width - Add animations for enter/exit effects
- Apply CSS text stroke for better visibility:
<div
style={{
WebkitTextStroke: '4px black',
paintOrder: 'stroke',
}}
>
{text}
</div>See also
- Transcribing audio - Generate captions from audio
Caption- The caption data structurecreateTikTokStyleCaptions()- API reference<Sequence>- Sequence component reference