PTENES
MODULE 2.3

🎞️ Content Animation

9 templates for animating lists, cards, timers, notifications, particles, and sounds. Each one teaches a different pattern: stagger, 3D CSS, frame-derived state, simple physics, and perpetual loops.

9
Templates
45
Minutes
Inter
Level
Composition
Type
Timeline: stagger = i × 10 frames F0 F10 F20 F30 F40 ✅ Item 1 — from=0 ✅ Item 2 — from=10 ✅ Item 3 — from=20 ✅ Item 4 — from=30
1

🎞️ Overview — Temporal composition patterns

The "Content Animation" category groups templates that go beyond animating a single element—they orchestrate multiple elements over time. The central pattern is stagger via Sequence with from staggered, but the category also covers CSS 3D, physics simulation, and perpetual loops.

🎯 The 4 patterns in this category

1. Sequential Stagger

animated-list, notification-pop, progress-steps — multiple elements with scaled from.

2. CSS 3D

card-flip, rotating-carousel — perspective + rotateY for depth.

3. Frame-derived state

countdown-timer — the displayed number is calculated directly from the frame, without useState.

4. Physics simulation

particle-explosion, sound-wave — position/height calculated with per-frame arithmetic.

9 templates
Category
4 patterns
Stagger/3D/Derived/Physics
Sequence
Core tool
No useState
Everything comes from the frame
2

📋 animated-list — staggered items

O animated-list.tsx is the reference template for staggering multiple elements. Each item in a list appears with a delay proportional to its index, creating that “items keep arriving” feeling that’s ubiquitous in explainer videos and animated slides.

animated-list — stagger with Sequence templates/animated-list.tsx
const DELAY_PER_ITEM = 10; // frames entre cada item

{items.map((item, i) => (
  <Sequence key={i} from={i * DELAY_PER_ITEM}>
    <ListItem item={item} />
  </Sequence>
))}

// Dentro de ListItem: frame é LOCAL (começa em 0)
const frame = useCurrentFrame(); // 0, 1, 2...
const opacity = interpolate(frame, [0, 15], [0, 1],
  { extrapolateRight: "clamp" });
const y = interpolate(frame, [0, 15], [20, 0],
  { extrapolateRight: "clamp" });

📊 The power of the LOCAL frame within the Sequence

When you use <Sequence from={i * 10}>, the child component receives useCurrentFrame() starting at 0 when the sequence begins. This means each ListItem can use the same animation logic — you don't need to subtract the offset manually. Remotion handles the shift.

✓ Correct: Sequence + local frame

<Sequence from={i * 10}>
  <Item />  // frame começa em 0
</Sequence>

Each Item has the same logic — clean and reusable.

✗ Incorrect: manually subtracting the offset

const localFrame = frame - i * 10;
if (localFrame < 0) return null;

Verbose, prone to edge-case bugs (negative frame).

Pattern
Sequence from=i×delay
Local frame
Starts at 0 in the child
Animation
fade + translateY
Reuse in
bullets, toasts, steps
3

🃏 card-flip — CSS 3D in Remotion

O card-flip.tsx shows how to implement CSS 3D inside Remotion: two faces of a card positioned with backfaceVisibility: "hidden", the container with perspective, and the interpolated rotation from 0° to 180°.

card-flip — CSS 3D rotation templates/card-flip.tsx
const frame = useCurrentFrame();

const rotation = interpolate(
  frame, [15, 45], [0, 180],
  { extrapolateRight: "clamp" }
);

return (
  {/* Contêiner: define a perspectiva */}
  <div style={{ perspective: 800, transformStyle: "preserve-3d" }}>
    {/* Frente */}
    <div style={{
      backfaceVisibility: "hidden",
      transform: `rotateY(${rotation}deg)`,
      position: "absolute", inset: 0
    }}>Face A</div>
    {/* Verso — começa já virado 180° */}
    <div style={{
      backfaceVisibility: "hidden",
      transform: `rotateY(${rotation - 180}deg)`,
      position: "absolute", inset: 0
    }}>Face B</div>
  </div>
);

💡 Why backfaceVisibility: "hidden"?

Without this property, you'd see both sides at once — the front appearing mirrored in the back and vice versa. With hidden, each face is visible only when it’s facing the viewer. That’s the secret behind any flip card.

CSS 3D
perspective + preserve-3d
Faces
backfaceVisibility: hidden
Rotation
0° → 180°
Back
rotation - 180°
4

⏳ countdown-timer — frame-derived state

O countdown-timer.tsx illustrates a core concept of Remotion: the displayed value is always a direct function of the frame, without useState or effects. The displayed number is calculated from the current frame and the duration/fps parameters.

countdown-timer — frame derivation templates/countdown-timer.tsx
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();

// segundos restantes — derivados do frame
const secondsLeft = Math.ceil(
  (durationInFrames - frame) / fps
);

// "GO" quando chega a 0
const label = secondsLeft > 0 ? String(secondsLeft) : "GO!";

// spring de entrada para cada mudança de número
const frameInSegment = frame % fps; // 0→fps a cada segundo
const scale = spring({ fps, frame: frameInSegment,
  config: { damping: 10, stiffness: 150 }
});

Countdown event sequence (30fps, 5s):

F0

Frame 0 — displays "5"

(150 - 0) / 30 = 5 → Math.ceil(5) = 5. Spring enters with a scale-pop.

F30

Frame 30 — switches to "4"

(150 - 30) / 30 = 4 → number changes, spring restarts (frame % fps = 0).

F150

Frame 150 — displays "GO!"

secondsLeft ≤ 0 → label = "GO!". No state, no setInterval.

Derivation
Math.ceil(remaining/fps)
No state
Zero useState
Spring
frame % fps for pop
GO!
Simple conditional
5

💥 particle-explosion — deterministic 2D physics

O particle-explosion.tsx simulates a particle explosion with a physical trajectory: starting position at the center, velocity with a random (seeded) angle, and simple gravity. Everything is calculated frame by frame — with no accumulated state.

particle-explosion — seeded physics templates/particle-explosion.tsx
const PARTICLE_COUNT = 40;

{Array.from({ length: PARTICLE_COUNT }, (_, i) => {
  // velocidade e ângulo DETERMINÍSTICOS
  const angle = random(`angle-${i}`) * 2 * Math.PI;
  const speed = random(`speed-${i}`) * 8 + 2;

  const vx = Math.cos(angle) * speed;
  const vy = Math.sin(angle) * speed;

  // posição frame-a-frame com gravidade
  const x = cx + vx * frame;
  const y = cy + vy * frame + 0.15 * frame * frame; // g

  const opacity = interpolate(frame, [20, 45], [1, 0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
})}

⚠️ No seeded random() = corrupted video

If you use Math.random() here, each calculated frame produces a different trajectory — in Remotion’s parallel render, the particles “jump” to random positions. The final video looks corrupted. random("seed") ensures frame 42 always has the same particles in the same places.

random(seed)
Deterministic trajectory
Physics
v × frame + ½g×frame²
Fade-out
interpolate final frames
No state
Recalculated position
6

🎵 sound-wave & text-highlight — loops and focus

Two templates that demonstrate complementary patterns: sound-wave.tsx creates an audio visualizer (bars that oscillate with Math.sin) and text-highlight.tsx highlights one word at a time in sync with the frame — like karaoke or guided reading.

sound-wave

{bars.map((_, i) => {
  const h = Math.sin(
    frame / 8         // velocidade
    + i * 0.4       // fase
  ) * 0.5 + 0.5;   // 0 a 1
  return <rect height={h * maxH}/>;
})}

text-highlight

const words = text.split(" ");
const framesPerWord = fps / 2;
const activeIndex = Math.floor(
  frame / framesPerWord
);

{words.map((w, i) => (
  <span style={{
    color: i === activeIndex
      ? "#60a5fa" : "white"
  }}>{w}</span>
))}

💡 text-highlight: sync with audio

The same pattern of Math.floor(frame / framesPerWord) can be adapted to sync words with transcript timestamps. In the advanced T3 modules, you’ll see how to combine this template with @remotion/captions for automatic karaoke.

sound-wave
Math.sin + phase
highlight
Math.floor(frame/rate)
Loop
Sin: periodic
Synchronization
fps/words/sec
7

📚 Complete Library — Content Animation

TemplateDescriptionCentral pattern
animated-list.tsx List with staggered timing Sequence from=i×delay
card-flip.tsx Card with 3D flip rotateY + backfaceVisibility
countdown-timer.tsx 5-4-3-2-1-GO Countdown Math.ceil(remaining/fps)
notification-pop.tsx Spring-stacked toasts Nested sequence + spring
particle-explosion.tsx Explosion with 2D physics random(seed) + vx/vy×frame
progress-steps.tsx Steps with an active indicator Math.floor(frame/stepDuration)
rotating-carousel.tsx Rotating 3D carousel rotateY per slot + loop
sound-wave.tsx Audio bar visualizer Math.sin + phase per bar
text-highlight.tsx Active word per second Math.floor(frame/fps×rate)

📌 Module summary

✓
Sequence + local frame — the correct stagger pattern; don't subtract the offset manually.
✓
3D CSS = perspective + backfaceVisibility — card flip and carousel without a 3D library.
✓
Frame-derived state — countdown without useState; everything calculated from Math.ceil(remaining/fps).
✓
random(seed) for physics — particles reproducible across parallel renders.
✓
Math.sin for perpetual loops — sound wave and any continuous oscillation with no beginning or end.

Next module:

2.4 — Image & Media: galleries, sliders, comparators, and frames for photos in video.