---
name: remotion-frame-driven-animation
source: https://app.decimal.ai/s/remotion-frame-driven-animation@1/SKILL.md
source_sha256: 436f1d6e9b4c
---

# Remotion frame-driven animation

## Contract

In Remotion, motion is rendered frame-by-frame on a server, not played in a browser. Every animated
value MUST be a pure function of the current frame. Apply this whenever you write a Remotion component
that moves, fades, scales, rotates, counts, or otherwise changes over time.

## Rules

1. **Drive from the frame.** Read the current frame with `useCurrentFrame()` (imported from `remotion`)
   and compute every animated style from it. The component must be a pure function of that frame — the
   same frame always yields the same output.

2. **No browser-time animation.** These are forbidden because they are not frame-deterministic and will
   flicker or drop out when rendered:
   - CSS `transition`, CSS `animation`, `@keyframes`
   - Tailwind `animate-*` utility classes
   - `setTimeout`, `setInterval`, `requestAnimationFrame`, `Date.now()`, `performance.now()`
   - `useState` + `useEffect` timers that advance the animation
   - animation libraries that run on wall-clock time (e.g. framer-motion's time-based `animate`)

3. **Map ranges with `interpolate`.** For linear/eased mapping call `interpolate` with **four positional
   arguments in this exact order**: `interpolate(frame, inputRange, outputRange, options?)`. The input
   and output ranges are plain arrays. Never use an object-configuration form.

4. **Clamp the edges.** `interpolate` extrapolates by default (`'extend'`), so values run past the range.
   Stop that with the options object keys `extrapolateLeft` and `extrapolateRight` set to the string
   `'clamp'`. Use `'clamp'` for any value that must not overshoot (opacity, progress 0→1).

5. **Springs take one object.** Call `spring` with a **single object argument** — `spring({ frame, fps })`
   — never positionally. Tune physics through a nested `config` object using only the keys `damping`,
   `stiffness`, `mass`, and `overshootClamping` (defaults `10 / 100 / 1 / false`). Stretch a spring to a
   length with `durationInFrames`; start it late with `delay`.

6. **Work in seconds × fps.** Read `fps` from `useVideoConfig()` and convert any second-based duration to
   frames by multiplying by `fps` (e.g. `1.5 * fps`). Never hard-code milliseconds or a raw frame count
   that assumes a fixed fps.

7. **Ease through the option, not CSS.** Apply easing via the `easing` key of `interpolate`'s options
   object using the `Easing` module (imported from `remotion`): compose a convexity with a curve, e.g.
   `Easing.inOut(Easing.quad)`, or `Easing.bezier(0.8, 0.22, 0.96, 0.65)`. Never a CSS timing-function
   string.

8. **Delay by offsetting the frame.** To start an element's motion later, subtract frames from the input
   (`frame - delayInFrames`) or pass `delay` to `spring` — do not schedule with a timer or CSS delay.

## Worked examples

**Fade — CSS transition → frame-driven interpolate**
```tsx
// BEFORE (does not render): <div style={{ opacity: 1, transition: "opacity 2s" }}>Welcome</div>
// AFTER
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], { extrapolateRight: "clamp" });
return <div style={{ opacity }}>Welcome</div>;
```

**Pop — framer-motion → spring**
```tsx
// BEFORE: <motion.div animate={{ scale: 1 }} transition={{ type: "spring" }} />
// AFTER
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, config: { damping: 12, stiffness: 200 } });
return <div style={{ transform: `scale(${scale})` }} />;
```

**interpolate signature — object form → four positional args**
```tsx
// BEFORE (wrong shape): interpolate({ value: frame, inputRange: [0, 30], outputRange: [0, 100] })
// AFTER
const x = interpolate(frame, [0, 30], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
```

**Delay — setTimeout → frame offset**
```tsx
// BEFORE: setTimeout(() => setShown(true), 500)
// AFTER
const appear = spring({ frame: frame - 0.5 * fps, fps });
```

**Easing — CSS timing string → Easing module**
```tsx
// BEFORE: style={{ transition: "left 1s cubic-bezier(0.8,0.22,0.96,0.65)" }}
// AFTER
import { interpolate, Easing } from "remotion";
const left = interpolate(frame, [0, fps], [0, 200], {
  easing: Easing.inOut(Easing.quad),
  extrapolateRight: "clamp",
});
```

## Edge cases & exceptions

- **Static styles are fine.** Colors, borders, and layout that do not change over time need no frame math.
- **Inside a `<Sequence>`, `useCurrentFrame()` is local** (starts at 0 when the sequence begins) — build the
  animation against that local frame; do not add the sequence's `from` back in.
- **Springs output ~0→1** — map them to a real range by feeding the spring value into `interpolate` (e.g.
  rotation `interpolate(s, [0, 1], [0, 360])`).
- **Third-party canvas/WebGL** (Three.js, Lottie) still advances off `useCurrentFrame()` — drive its
  progress prop from the frame, never off its own internal clock.

## Do / Don't

- **Do** compute animated styles from `useCurrentFrame()`. **Don't** use CSS `transition`/`@keyframes`.
- **Do** call `interpolate(frame, inRange, outRange, opts)` with four positional args. **Don't** pass an
  options/config object as the only argument.
- **Do** clamp with `extrapolateRight: 'clamp'`. **Don't** leave a bounded value on the default `'extend'`.
- **Do** call `spring({ frame, fps })`. **Don't** call `spring(frame, fps)` positionally.
- **Do** derive durations as `seconds * fps`. **Don't** hard-code milliseconds or assume 30fps.
- **Do** ease via `Easing.inOut(Easing.quad)`. **Don't** use a CSS `cubic-bezier()` timing string.

## Common mistakes

- Reaching for CSS transitions / Tailwind `animate-*` out of React habit — they render blank or flicker.
- Writing `interpolate` in an object-config form instead of the four positional arguments.
- Forgetting to clamp, so opacity/progress shoots past 1 or below 0 outside the range.
- Calling `spring` positionally, or inventing config keys instead of `damping`/`stiffness`/`mass`.
- Timing in milliseconds (`frame / 30`, `* 1000`) instead of `seconds * fps` from `useVideoConfig()`.
- Using `setTimeout`/`requestAnimationFrame`/`Date.now()` to sequence motion.

## Quick checklist

- [ ] Every animated value is a function of `useCurrentFrame()`.
- [ ] `interpolate(frame, inputRange, outputRange, options)` — four positional args, arrays for ranges.
- [ ] Bounded values clamped with `extrapolateLeft`/`extrapolateRight: 'clamp'`.
- [ ] `spring({ frame, fps, config: { damping, stiffness, mass } })` — single object.
- [ ] Durations are `seconds * fps` with `fps` from `useVideoConfig()`.
- [ ] Easing via `Easing.*` in the `easing` option; no CSS transitions/timers.
