R RenderComp
remotion text-animation animation typography tutorial

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 simultaneous
  • staggerFrames: 6 — comfortable reading pace
  • staggerFrames: 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:

StylemassstiffnessdampingCharacter
Snappy wipe0.812020Sharp, no bounce
Natural word entrance0.910014Slight bounce, energetic
Luxury mask reveal1.08016Smooth, weighted
Playful letter bounce0.714010Clear bounce, light
Dramatic slow entrance1.26018Heavy, 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 →