Skip to main content
Impact: HIGH - Fixes common text animation bugs and significantly improves readability
This skill covers patterns for professional text animations, including typewriter effects, cursor blinking, word carousels, and text highlighting.

Typewriter Effect - Use String Slicing

Always use string slicing for typewriter effects. Never use per-character opacity.
{
  text
    .split("")
    .map((char, i) => (
      <span style={{ opacity: i < typedCount ? 1 : 0 }}>{char}</span>
    ));
}
<span>|</span>;
Per-character opacity creates invisible characters that still occupy space, causing the cursor to appear in the wrong position. Use .slice() instead.
Blinking cursors should fade smoothly, not flash on/off abruptly.
const caretVisible = Math.floor(frame / 15) % 2 === 0;
<span style={{ opacity: caretVisible ? 1 : 0 }}>|</span>;
Smooth fading creates a more polished, professional appearance compared to harsh on/off toggling.
Prevent layout shifts by using the longest word to set container width.
<div style={{ position: "relative" }}>
  <span>{WORDS[currentIndex]}</span>
</div>
The invisible element reserves space based on the longest word, while the absolutely positioned visible word sits on top. This prevents the container from resizing as words change, eliminating jarring layout shifts.

Text Highlight - Two Layer Crossfade

Use overlapping layers for smooth highlight transitions.
const typedOpacity = interpolate(
  frame,
  [highlightStart - 8, highlightStart + 8],
  [1, 0],
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const finalOpacity = interpolate(
  frame,
  [highlightStart, highlightStart + 8],
  [0, 1],
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);

{/* Typing layer */}
<div style={{ opacity: typedOpacity }}>{typedText}</div>;

{/* Final layer with highlight */}
<div style={{ position: "absolute", inset: 0, opacity: finalOpacity }}>
  <span>{preText}</span>
  <span style={{ backgroundColor: COLOR_HIGHLIGHT }}>{HIGHLIGHT_WORD}</span>
  <span>{postText}</span>
</div>;
The 8-frame overlap creates a smooth crossfade between the typing state and the highlighted state, avoiding abrupt transitions.

Key Patterns

For typewriter effects, always use:
const displayed = fullText.slice(0, charCount);
Never use per-character opacity or visibility.
Use interpolation with at least 3 keyframes for smooth fade:
interpolate(
  frame % CYCLE,
  [0, CYCLE / 2, CYCLE],
  [1, 0, 1]
)
Reserve space with invisible elements:
<div style={{ visibility: "hidden" }}>{longestContent}</div>
<div style={{ position: "absolute" }}>{visibleContent}</div>
Overlap opacity transitions for smooth blends:
// Layer 1: fade out from frame 52-60
// Layer 2: fade in from frame 56-64
// 4-frame overlap creates smooth crossfade

Common Mistakes to Avoid

Don’t use per-character opacity for typewriter effects - The cursor position will be wrong because invisible characters still take up space.
Don’t use binary (0 or 1) opacity for cursor blink - Use smooth interpolation instead for a professional look.
Don’t let word carousels jump around - Use the longest word to establish stable container dimensions.
Don’t switch highlights abruptly - Use two-layer crossfade technique for smooth transitions.

Typography Best Practices

1

Choose Readable Fonts

Use web-safe fonts or import from Google Fonts. Ensure sufficient weight and size for video compression.
2

Plan Timing Carefully

Typewriter speed: ~3-5 characters per second (at 30fps = ~6-10 frames per char)Cursor blink cycle: ~16 frames for natural rhythm
3

Test Readability

Preview at actual output resolution. Text that looks good in the editor may be too small when rendered.

Spring Physics

Add bounce to text entrances

Sequencing

Coordinate multiple text elements

Build docs developers (and LLMs) love