Text Reveal Animations in Remotion: A Complete Implementation Guide
Text Reveal Animations in Remotion: A Complete Implementation Guide
Text animation is the signature move of broadcast motion graphics. In television titles, film credits, corporate presentations, and social video, how text arrives on screen communicates as much as what the text says. A sharp wipe reveal signals precision. A word-by-word cascade reads as thoughtful and rhythmic. Letter-by-letter scrambles feel technical or dramatic. Each technique has a personality — and in Remotion, you can implement all of them with a handful of composable React components.
This guide covers the five most effective text reveal patterns for programmatic video: clip-path wipe, opacity-and-translate word reveal, letter-by-letter stagger, mask reveal, and scramble. For each, you will get the full working implementation using Remotion’s useCurrentFrame, spring, and interpolate APIs.
Prerequisites: The Core APIs
All text reveal techniques in this guide rely on three Remotion primitives. If you are already comfortable with them, skip to the first pattern. If not, here is the minimum you need to know.
useCurrentFrame() — Returns the current frame number as an integer, starting at 0. Every animated value in Remotion is computed from this number. Inside a <Sequence>, the frame number is relative to the sequence’s start, not the global timeline.
spring({ frame, fps, config, from, to }) — Produces a physics-simulated value that moves from from to to with mass, stiffness, and damping. Returns a number. The idiomatic pattern is to produce a 0 → 1 progress value and map it to CSS properties via interpolate.
interpolate(value, inputRange, outputRange, options) — Maps a number from one range to another. Used to translate a 0 → 1 spring output into concrete CSS values (pixels, degrees, opacity). The extrapolateRight: 'clamp' option is almost always appropriate to prevent values from exceeding their intended bounds.
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';
import { Sequence, AbsoluteFill } from 'remotion';
Pattern 1: Clip-Path Wipe Reveal
The wipe reveal is the workhorse of broadcast text animation. Text appears as if being uncovered by a sliding mask — clean, directional, and legible even at small sizes. Technically, it is implemented with clip-path and a width percentage that animates from 0% to 100%.
How It Works
CSS clip-path: inset(0 X% 0 0) clips the element, hiding everything to the right of X%. Animating X from 100 to 0 reveals the text from left to right.
Implementation
// components/WipeReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';
interface WipeRevealProps {
text: string;
fontSize?: number;
color?: string;
delay?: number;
}
export const WipeReveal: React.FC<WipeRevealProps> = ({
text,
fontSize = 72,
color = '#ffffff',
delay = 0,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: Math.max(0, frame - delay),
fps,
config: { mass: 0.8, stiffness: 120, damping: 20, overshootClamping: true },
});
// clip from right: 100% → 0% (fully hidden → fully revealed)
const clipRight = interpolate(progress, [0, 1], [100, 0]);
return (
<div
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize,
fontWeight: 700,
color,
clipPath: `inset(0 ${clipRight}% 0 0)`,
whiteSpace: 'nowrap',
lineHeight: 1.2,
}}
>
{text}
</div>
);
};
Staggered Multi-Line Wipe
For headlines with multiple lines, stagger each line’s entry with a frame delay:
const lines = ['The Future', 'of Video', 'Is Code.'];
export const MultiLineWipe: React.FC = () => (
<AbsoluteFill
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 80px',
gap: 8,
}}
>
{lines.map((line, i) => (
<WipeReveal
key={i}
text={line}
delay={i * 8} // 8 frames between each line
fontSize={80}
/>
))}
</AbsoluteFill>
);
The delay of i * 8 means the second line starts 8 frames after the first, and the third starts 16 frames after the first. At 30fps, this creates a crisp 267ms cascade.
Pattern 2: Word-by-Word Reveal
Word-by-word reveals are ideal for narration-style social video where each word corresponds to a beat in voiceover or music. Each word fades and slides into position independently, making the headline feel like it is being spoken aloud.
Implementation
The key is splitting the text into words, then computing a staggered spring for each word indexed by its position.
// components/WordByWordReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';
interface WordByWordRevealProps {
text: string;
staggerFrames?: number;
fontSize?: number;
color?: string;
}
export const WordByWordReveal: React.FC<WordByWordRevealProps> = ({
text,
staggerFrames = 5,
fontSize = 64,
color = '#ffffff',
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const words = text.split(' ');
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0 16px',
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize,
fontWeight: 700,
color,
lineHeight: 1.3,
}}
>
{words.map((word, i) => {
const wordProgress = spring({
frame: Math.max(0, frame - i * staggerFrames),
fps,
config: { mass: 0.9, stiffness: 100, damping: 14 },
});
return (
<span
key={i}
style={{
display: 'inline-block',
opacity: wordProgress,
transform: `translateY(${interpolate(
wordProgress,
[0, 1],
[28, 0]
)}px)`,
}}
>
{word}
</span>
);
})}
</div>
);
};
Usage
// Inside your composition:
<Sequence from={20} name="Headline" layout="none">
<div style={{ position: 'absolute', bottom: 240, left: 80, right: 80 }}>
<WordByWordReveal
text="Every frame is a React component."
staggerFrames={6}
fontSize={56}
/>
</div>
</Sequence>
The staggerFrames parameter is the key tuning dial. At 30fps:
staggerFrames: 3— very rapid, almost simultaneousstaggerFrames: 6— comfortable reading pacestaggerFrames: 10— deliberate, dramatic
Pattern 3: Letter-by-Letter Stagger
Letter-by-letter reveals feel more intimate and technical than word reveals. They work particularly well for short headlines (5–15 characters), brand names, or in motion graphics where the individual character is meaningful.
Implementation
// components/LetterByLetterReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';
interface LetterByLetterRevealProps {
text: string;
staggerFrames?: number;
fontSize?: number;
color?: string;
direction?: 'up' | 'down';
}
export const LetterByLetterReveal: React.FC<LetterByLetterRevealProps> = ({
text,
staggerFrames = 3,
fontSize = 96,
color = '#ffffff',
direction = 'up',
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const characters = text.split('');
return (
<div
style={{
display: 'flex',
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize,
fontWeight: 800,
color,
letterSpacing: '0.02em',
overflow: 'hidden',
}}
>
{characters.map((char, i) => {
const charProgress = spring({
frame: Math.max(0, frame - i * staggerFrames),
fps,
config: { mass: 0.7, stiffness: 140, damping: 12 },
});
const yOffset = direction === 'up'
? interpolate(charProgress, [0, 1], [40, 0])
: interpolate(charProgress, [0, 1], [-40, 0]);
return (
<span
key={i}
style={{
display: 'inline-block',
opacity: interpolate(charProgress, [0, 1], [0, 1], {
extrapolateRight: 'clamp',
}),
transform: `translateY(${yOffset}px)`,
// Preserve space characters
minWidth: char === ' ' ? '0.35em' : undefined,
}}
>
{char === ' ' ? ' ' : char}
</span>
);
})}
</div>
);
};
The direction: 'up' variant slides letters up from below — suitable for titles. The direction: 'down' variant drops letters from above — better for dramatic or heavy headlines. Space characters are preserved using a non-breaking space ( ) to maintain natural word spacing.
Pattern 4: Mask Reveal (Clip Container)
The mask reveal is a more elegant cousin of the wipe. Instead of animating clip-path on the text itself, you place the text inside a container with overflow: hidden and animate the text’s translateY position — as if text is sliding up from behind a physical mask.
This is the technique used in luxury brand titles and premium editorial video. It feels more three-dimensional than a flat wipe.
// components/MaskReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';
interface MaskRevealProps {
text: string;
fontSize?: number;
color?: string;
delay?: number;
}
export const MaskReveal: React.FC<MaskRevealProps> = ({
text,
fontSize = 80,
color = '#ffffff',
delay = 0,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: Math.max(0, frame - delay),
fps,
config: { mass: 1, stiffness: 80, damping: 16, overshootClamping: true },
});
const translateY = interpolate(progress, [0, 1], [fontSize * 1.2, 0]);
return (
<div
style={{
overflow: 'hidden',
height: fontSize * 1.4, // container clips the text
lineHeight: `${fontSize * 1.4}px`,
}}
>
<div
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize,
fontWeight: 700,
color,
transform: `translateY(${translateY}px)`,
whiteSpace: 'nowrap',
}}
>
{text}
</div>
</div>
);
};
Stacked Mask Lines
Mask reveals become especially effective when multiple lines enter sequentially:
const taglines = ['Imagine.', 'Build.', 'Ship.'];
export const StackedMaskReveal: React.FC = () => (
<AbsoluteFill
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 80px',
}}
>
{taglines.map((line, i) => (
<MaskReveal
key={i}
text={line}
delay={i * 10}
fontSize={88}
/>
))}
</AbsoluteFill>
);
Pattern 5: Scramble Effect
The scramble effect randomises each character through a pool of glyphs before resolving to the correct letter. It reads as technical, cinematic, or cryptic — the style of choice for hacker-aesthetic titles, data reveal sequences, and dramatic introductions.
The key technical detail is that the scramble must be deterministic across renders. Remotion renders each frame independently — if you use Math.random() directly, each render pass produces different characters, which causes flickering in the output video. The fix is to use a seeded random function based on the frame number.
// utils/seededRandom.ts
export function seededRandom(seed: number): number {
// Simple LCG — deterministic for a given seed
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
export function randomChar(seed: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
return chars[Math.floor(seededRandom(seed) * chars.length)];
}
// components/ScrambleReveal.tsx
import { useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
import { randomChar } from '../utils/seededRandom';
interface ScrambleRevealProps {
text: string;
scrambleDurationFrames?: number;
fontSize?: number;
color?: string;
accentColor?: string;
}
export const ScrambleReveal: React.FC<ScrambleRevealProps> = ({
text,
scrambleDurationFrames = 30,
fontSize = 72,
color = '#ffffff',
accentColor = '#00ff88',
}) => {
const frame = useCurrentFrame();
const characters = text.toUpperCase().split('');
return (
<div
style={{
display: 'flex',
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize,
fontWeight: 800,
letterSpacing: '0.08em',
}}
>
{characters.map((targetChar, i) => {
// Each character resolves after a staggered delay
const charResolveFrame = Math.round(
(i / characters.length) * scrambleDurationFrames
);
const isResolved = frame >= charResolveFrame;
// During scramble phase, cycle through random characters
const displayChar = isResolved
? targetChar
: randomChar(frame * 100 + i);
// Scrambling characters are shown in accent colour; resolved ones in primary colour
const charColor = isResolved ? color : accentColor;
return (
<span
key={i}
style={{
color: charColor,
display: 'inline-block',
minWidth: targetChar === ' ' ? '0.35em' : undefined,
// Slightly fade in the scramble phase
opacity: targetChar === ' ' ? 1 : interpolate(
frame,
[0, 8],
[0, 1],
{ extrapolateRight: 'clamp' }
),
}}
>
{targetChar === ' ' ? ' ' : displayChar}
</span>
);
})}
</div>
);
};
The seededRandom(frame * 100 + i) call produces a different random character each frame for each character position, but the same character every time a given frame is rendered. This is the essential property for deterministic video output.
Combining Patterns: A Broadcast-Style Title Sequence
The patterns above are most powerful when layered. Here is a complete title sequence that combines a scramble reveal on the main headline, a mask reveal on the subtitle, and a word-by-word reveal on a tagline — each staggered to create a multi-beat entrance.
// compositions/TitleSequence.tsx
import { AbsoluteFill, Sequence } from 'remotion';
import { ScrambleReveal } from '../components/ScrambleReveal';
import { MaskReveal } from '../components/MaskReveal';
import { WordByWordReveal } from '../components/WordByWordReveal';
export const TitleSequence: React.FC = () => (
<AbsoluteFill
style={{
background: '#0a0a0a',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 80px',
gap: 24,
}}
>
{/* Headline — scramble, resolves over 30 frames */}
<Sequence from={0} name="Headline" layout="none">
<ScrambleReveal
text="RENDERCOMP"
scrambleDurationFrames={30}
fontSize={96}
accentColor="#00ff88"
/>
</Sequence>
{/* Subtitle — mask reveal, starts at frame 20 */}
<Sequence from={20} name="Subtitle" layout="none">
<MaskReveal
text="Programmatic video at scale."
fontSize={36}
color="rgba(255,255,255,0.7)"
/>
</Sequence>
{/* Tagline — word by word, starts at frame 40 */}
<Sequence from={40} name="Tagline" layout="none">
<WordByWordReveal
text="Build once. Render everywhere."
staggerFrames={5}
fontSize={28}
color="rgba(255,255,255,0.45)"
/>
</Sequence>
</AbsoluteFill>
);
The use of layout="none" on each <Sequence> is important here. Because the parent AbsoluteFill uses flexbox to lay out the three text elements vertically, we do not want each <Sequence> to wrap its child in another AbsoluteFill (which uses position: absolute). Setting layout="none" makes the <Sequence> a timing-only wrapper with no visual container.
Spring Config Cheat Sheet for Text Animation
Different text reveals benefit from different spring configurations. Here are the settings used in production:
| Style | mass | stiffness | damping | Character |
|---|---|---|---|---|
| Snappy wipe | 0.8 | 120 | 20 | Sharp, no bounce |
| Natural word entrance | 0.9 | 100 | 14 | Slight bounce, energetic |
| Luxury mask reveal | 1.0 | 80 | 16 | Smooth, weighted |
| Playful letter bounce | 0.7 | 140 | 10 | Clear bounce, light |
| Dramatic slow entrance | 1.2 | 60 | 18 | Heavy, decisive |
All wipe and mask patterns should use overshootClamping: true — overshooting a clip-path width or a translateY position produces a visible glitch where text briefly extends beyond its container.
FAQ
Q: Why use Math.max(0, frame - delay) instead of the delay parameter in spring()?
Both approaches work, but Math.max(0, frame - delay) is more explicit about what is happening and is easier to reason about when you have many staggered elements. The delay parameter on spring() is equivalent — it internally clamps the frame to zero for the delay period. Either is correct; prefer consistency within your codebase.
Q: Will clip-path animate smoothly in the rendered video output?
Yes. Remotion computes the clip-path value frame by frame during rendering — it is not relying on CSS transitions. Every frame gets its own precise computed value, so the output is as smooth as your frame rate allows (30fps or 60fps, depending on your composition settings).
Q: How do I make the text hold still after the reveal instead of animating out?
The techniques above are entrance-only — once the spring settles, the value is constant and the text holds. You only need an exit animation if you explicitly want one. To exit, add a second spring driven by Math.max(0, frame - exitStartFrame) and subtract it from the enter progress.
Q: Can I combine the scramble effect with the word-by-word reveal?
Yes. Replace each <span> in the WordByWordReveal component with a <ScrambleReveal> that takes a single word. Set the scrambleDurationFrames to a short value (6–10 frames) so each word resolves quickly before the next word starts scrambling.
Q: My letter-by-letter reveal has visual gaps between characters. How do I fix this?
The gaps come from the display: 'inline-block' applied to each <span>. Inline-block elements inherit word-spacing and letter-spacing from their container. Ensure the parent div has no extra gap or letter-spacing, and use minWidth on space characters rather than relying on whitespace between <span> elements.
Q: What is the maximum number of characters that works well for letter-by-letter?
In practice, around 20–30 characters at staggerFrames: 3 keeps the total entrance within 2–3 seconds, which is appropriate for social video. Beyond 30 characters, the animation feels drawn out. For long text, switch to word-by-word reveal, which reads much faster for the same character count.
Take Text Animation Further with RenderComp
The patterns in this guide are the foundation of every text-heavy template in the RenderComp library. Every title card, lower third, subtitle overlay, and kinetic typography template uses these same spring + interpolate + Sequence primitives — tuned and ready to use.
Visit rendercomp.com to browse text animation templates. Open any template in your editor and you will find clean, readable components built exactly like the examples above — easy to adapt to your brand colours, copy, and timing requirements.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →