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:
Fetching captionsimport {useState ,useEffect ,useCallback } from 'react'; import {AbsoluteFill ,staticFile ,useDelayRender } from 'remotion'; import type {Caption } from '@remotion/captions'; export constMyComponent :React .FC = () => { const [captions ,setCaptions ] =useState <Caption [] | null>(null); const {delayRender ,continueRender ,cancelRender } =useDelayRender (); const [handle ] =useState (() =>delayRender ()); constfetchCaptions =useCallback (async () => { try { constresponse = awaitfetch (staticFile ('captions.json')); constdata = awaitresponse .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:
Creating caption pagesconst {pages } =useMemo (() => { returncreateTikTokStyleCaptions ({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:
Rendering caption pagesconstCaptionedContent :React .FC = () => { const {fps } =useVideoConfig (); return ( <AbsoluteFill > {pages .map ((page ,index ) => { constnextPage =pages [index + 1] ?? null; conststartFrame = (page .startMs / 1000) *fps ; constendFrame =Math .min (nextPage ? (nextPage .startMs / 1000) *fps :Infinity ,startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) *fps ); constdurationInFrames =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:
Rendering a caption page with word highlightingimport {AbsoluteFill ,useCurrentFrame ,useVideoConfig } from 'remotion'; import type {TikTokPage } from '@remotion/captions'; constHIGHLIGHT_COLOR = '#39E508'; constCaptionPage :React .FC <{page :TikTokPage }> = ({page }) => { constframe =useCurrentFrame (); const {fps } =useVideoConfig (); // Current time relative to the start of the sequence constcurrentTimeMs = (frame /fps ) * 1000; // Convert to absolute time by adding the page start constabsoluteTimeMs =page .startMs +currentTimeMs ; return ( <AbsoluteFill style ={{justifyContent : 'center',alignItems : 'center', }} > <div style ={{fontSize : 80,fontWeight : 'bold',textAlign : 'center', // Preserve whitespace in captionswhiteSpace : 'pre', }} > {page .tokens .map ((token ) => { constisActive =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
Full captioned video exampleimport {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'; constSWITCH_CAPTIONS_EVERY_MS = 1200; constHIGHLIGHT_COLOR = '#39E508'; constCaptionPage :React .FC <{page :TikTokPage }> = ({page }) => { constframe =useCurrentFrame (); const {fps } =useVideoConfig (); constcurrentTimeMs = (frame /fps ) * 1000; constabsoluteTimeMs =page .startMs +currentTimeMs ; return ( <AbsoluteFill style ={{justifyContent : 'center',alignItems : 'center', }} > <div style ={{fontSize : 80,fontWeight : 'bold',textAlign : 'center',whiteSpace : 'pre', }} > {page .tokens .map ((token ) => { constisActive =token .fromMs <=absoluteTimeMs &&token .toMs >absoluteTimeMs ; return ( <span key ={token .fromMs }style ={{color :isActive ?HIGHLIGHT_COLOR : 'white', }} > {token .text } </span > ); })} </div > </AbsoluteFill > ); }; export constCaptionedVideo :React .FC = () => { const [captions ,setCaptions ] =useState <Caption [] | null>(null); const {delayRender ,continueRender ,cancelRender } =useDelayRender (); const [handle ] =useState (() =>delayRender ()); const {fps } =useVideoConfig (); constfetchCaptions =useCallback (async () => { try { constresponse = awaitfetch (staticFile ('captions.json')); constdata = awaitresponse .json ();setCaptions (data );continueRender (handle ); } catch (e ) {cancelRender (e ); } }, [continueRender ,cancelRender ,handle ]);useEffect (() => {fetchCaptions (); }, [fetchCaptions ]); const {pages } =useMemo (() => { returncreateTikTokStyleCaptions ({captions :captions ?? [],combineTokensWithinMilliseconds :SWITCH_CAPTIONS_EVERY_MS , }); }, [captions ]); return ( <AbsoluteFill style ={{backgroundColor : 'black'}}> {pages .map ((page ,index ) => { constnextPage =pages [index + 1] ?? null; conststartFrame = (page .startMs / 1000) *fps ; constendFrame =Math .min (nextPage ? (nextPage .startMs / 1000) *fps :Infinity ,startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) *fps ); constdurationInFrames =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