Scene Transitions in Remotion: Complete TransitionSeries Guide with Custom Presentations
Scene Transitions in Remotion: Complete TransitionSeries Guide with Custom Presentations
Every multi-scene video needs an answer to the same question: what happens at the cut point? A hard cut is often the right answer — but when you want a crossfade, a slide, a wipe, or something entirely custom, you need scenes that overlap in time while an effect blends between them. In a timeline editor you drag a transition onto the cut; in Remotion, you express it in code.
The official @remotion/transitions package handles this with a single component, <TransitionSeries>, plus two composable pieces: presentations (what the transition looks like) and timings (how fast it progresses). You can use the five built-in effects as-is or write your own presentation in about thirty lines.
This guide covers the full API: setting up a TransitionSeries, the built-in presentations, spring versus linear timing, custom presentations, the duration math that trips everyone up the first time, and audio crossfades.
What @remotion/transitions Is and When to Use It
@remotion/transitions is a first-party Remotion package. Install it with the Remotion CLI so the version matches the rest of your project:
npx remotion add @remotion/transitions
The package does three things for you:
- Sequencing — it lays out scenes back to back, like
<Series>, with no manualfromoffsets. - Overlapping — at each transition, it shifts the next scene backward in time so both scenes render simultaneously during the transition window.
- Presentation — it wraps the exiting and entering scenes in a component that receives a progress value from 0 to 1 and applies the visual effect.
If your video only needs hard cuts, you do not need this package — plain <Series> or <Sequence> components are simpler and the timing model is covered in our Sequence and Series timing guide. You could hand-roll transitions with overlapping <Sequence> components, but you would end up reimplementing the overlap math, progress calculation, and entering/exiting distinction that TransitionSeries already provides.
Setting Up TransitionSeries: Sequences vs Transitions
A <TransitionSeries> contains two kinds of children that alternate: <TransitionSeries.Sequence> (a scene, with an explicit durationInFrames) and <TransitionSeries.Transition> (the effect between two scenes). Here is a minimal two-scene crossfade:
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { AbsoluteFill } from "remotion";
const Scene: React.FC<{ label: string; background: string }> = ({
label,
background,
}) => {
return (
<AbsoluteFill
style={{
background,
justifyContent: "center",
alignItems: "center",
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 100,
fontWeight: 700,
color: "#ffffff",
}}
>
{label}
</AbsoluteFill>
);
};
export const CrossfadeDemo: React.FC = () => {
return (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<Scene label="Scene A" background="#0b84ff" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<Scene label="Scene B" background="#111827" />
</TransitionSeries.Sequence>
</TransitionSeries>
);
};
A few structural rules to know:
- Children are absolutely positioned. Each
<TransitionSeries.Sequence>behaves like an<AbsoluteFill>— scenes stack on top of each other during the overlap. - A transition must sit between two sequences. You cannot start or end the series with a transition, and two transitions cannot be adjacent.
- Every transition needs both props.
presentationdefines the visual effect;timingdefines the duration and progress curve. There are no defaults for either. - A transition must be shorter than both neighboring scenes. If a 15-frame transition touches a 10-frame scene, Remotion throws an error — the scene would be entering and exiting at once.
Built-in Presentations: Fade, Slide, Wipe, Flip, Clock Wipe
Each built-in presentation lives in its own subpath, which keeps your bundle small:
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
import { flip } from "@remotion/transitions/flip";
import { clockWipe } from "@remotion/transitions/clock-wipe";
What each one does, and the options it takes:
| Presentation | Effect | Key options |
|---|---|---|
fade() | Entering scene fades in over the exiting one | — |
slide({ direction }) | Entering scene pushes the exiting one out | "from-left", "from-right", "from-top", "from-bottom" |
wipe({ direction }) | Entering scene slides over the exiting one | Same four, plus diagonals like "from-top-left" (8 total) |
flip({ direction, perspective? }) | 3D card flip revealing the next scene | Four directions; perspective defaults to 1000 |
clockWipe({ width, height }) | Circular sweep reveals the next scene | Requires the composition dimensions |
clockWipe is the one that needs extra care: it requires the composition’s width and height, which you should read from useVideoConfig() rather than hardcoding:
import { TransitionSeries, springTiming, linearTiming } from "@remotion/transitions";
import { slide } from "@remotion/transitions/slide";
import { clockWipe } from "@remotion/transitions/clock-wipe";
import { useVideoConfig } from "remotion";
export const MultiTransitionDemo: React.FC = () => {
const { width, height } = useVideoConfig();
return (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Intro" background="#0b84ff" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={slide({ direction: "from-right" })}
timing={springTiming({ config: { damping: 200 } })}
/>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Features" background="#111827" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={clockWipe({ width, height })}
timing={linearTiming({ durationInFrames: 25 })}
/>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Outro" background="#065f46" />
</TransitionSeries.Sequence>
</TransitionSeries>
);
};
The difference between slide and wipe: slide moves both scenes (the new one pushes the old one out of frame), while wipe keeps the old scene static and slides the new one over it like a card.
There is also none() (from @remotion/transitions/none), a presentation with no visual effect — useful when you need a timed overlap without visuals, like an audio-only crossfade over a hard cut.
Timing Functions: springTiming vs linearTiming
The timing prop controls how presentationProgress moves from 0 to 1.
linearTiming progresses at constant speed over a fixed number of frames, with an optional easing curve:
import { linearTiming } from "@remotion/transitions";
import { Easing } from "remotion";
// Constant speed
linearTiming({ durationInFrames: 20 });
// Eased — starts slow, accelerates
linearTiming({
durationInFrames: 20,
easing: Easing.in(Easing.ease),
});
springTiming drives progress with spring physics, which gives transitions a natural acceleration and settle:
import { springTiming } from "@remotion/transitions";
springTiming({
config: { damping: 200 },
durationInFrames: 30,
durationRestThreshold: 0.001,
});
Three things worth knowing about springTiming:
- Omit
durationInFramesand the transition lasts as long as the spring takes to settle, which depends on the config and your composition’sfps. - Pass
durationInFramesand the spring curve is stretched to fit that duration exactly — useful when your duration math (next section) needs predictable numbers. - The default rest threshold can cut off the animation’s last visible moments.
durationRestThreshold: 0.001avoids a noticeable snap at the end, at the cost of a marginally longer transition.
A damping: 200 config gives a smooth, no-overshoot curve that works for most scene transitions. Lower damping values overshoot, which looks great on slide but strange on fade. To build intuition for spring configs, see our spring animation guide.
Building a Custom Presentation from Scratch
A presentation is just an object with two fields: a React component and the props to pass it. The component receives four props from the transition machinery:
children— the scene markup being wrappedpresentationDirection—"entering"or"exiting", telling you which of the two scenes this instance is wrappingpresentationProgress— a number from 0 to 1 driven by your timing functionpassedProps— your own type-safe configuration
The key mental model: during a transition, your component is rendered twice — once wrapping the exiting scene, once wrapping the entering scene. You decide what each side does. Here is a complete circle-reveal presentation, where the new scene expands from a growing circle in the center:
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import React, { useMemo } from "react";
import { AbsoluteFill } from "remotion";
type CircleRevealProps = {
width: number;
height: number;
};
const CircleRevealPresentation: React.FC<
TransitionPresentationComponentProps<CircleRevealProps>
> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
// Radius that fully covers the frame, reached at progress = 1
const finishedRadius =
Math.sqrt(passedProps.width ** 2 + passedProps.height ** 2) / 2;
const radius = finishedRadius * presentationProgress;
const style: React.CSSProperties = useMemo(() => {
return {
width: "100%",
height: "100%",
// The exiting scene renders untouched underneath;
// the entering scene is clipped to a growing circle.
clipPath:
presentationDirection === "exiting"
? undefined
: `circle(${radius}px at 50% 50%)`,
};
}, [presentationDirection, radius]);
return (
<AbsoluteFill>
<AbsoluteFill style={style}>{children}</AbsoluteFill>
</AbsoluteFill>
);
};
export const circleReveal = (
props: CircleRevealProps
): TransitionPresentation<CircleRevealProps> => {
return { component: CircleRevealPresentation, props };
};
Usage is identical to any built-in presentation:
const { width, height } = useVideoConfig();
<TransitionSeries.Transition
presentation={circleReveal({ width, height })}
timing={springTiming({ config: { damping: 200 }, durationInFrames: 25 })}
/>;
From this skeleton you can build almost anything: swap the clipPath for a filter: blur() that resolves as progress increases, animate scale and opacity for a zoom-punch cut, or apply different effects to each direction. Because progress comes from the timing function, one presentation works with both linear and spring timing.
Duration Math: Why Overlaps Shorten Your Video (and How to Fix It)
This is the number one source of confusion with TransitionSeries. Transitions overlap adjacent scenes, so the total length of your series is shorter than the sum of your sequence durations:
total = sum(scene durations) − sum(transition durations)
Two 60-frame scenes with a 15-frame fade produce a 105-frame video, not 120. Three 90-frame scenes with two 20-frame transitions produce 230 frames, not 270.
If your <Composition> declares durationInFrames as the naive sum, the series finishes early and the last frames of your render are empty. If you eyeball a shorter number, you risk cutting off the final scene. The fix: compute the duration instead of guessing it, using the getDurationInFrames() method every timing object exposes:
import { CalculateMetadataFunction, Composition } from "remotion";
import { linearTiming } from "@remotion/transitions";
const SCENE_DURATIONS = [90, 120, 90];
const TRANSITION_DURATION = 20;
const transitionTiming = linearTiming({
durationInFrames: TRANSITION_DURATION,
});
const calculateMetadata: CalculateMetadataFunction<
Record<string, unknown>
> = () => {
const fps = 30;
const transitionFrames = transitionTiming.getDurationInFrames({ fps });
const scenes = SCENE_DURATIONS.reduce((sum, d) => sum + d, 0);
const transitions = transitionFrames * (SCENE_DURATIONS.length - 1);
return {
fps,
durationInFrames: scenes - transitions, // 300 − 40 = 260
};
};
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="TransitionsDemo"
component={MultiTransitionDemo}
width={1920}
height={1080}
fps={30}
durationInFrames={260}
calculateMetadata={calculateMetadata}
/>
);
};
getDurationInFrames({ fps }) matters most with springTiming: without an explicit durationInFrames, the real duration is derived from the physics and the frame rate, and this method is the only reliable way to get it. If scene durations come from data — audio lengths, API responses — the same pattern extends naturally; see our calculateMetadata guide.
One more constraint while doing this math: a middle scene with transitions on both sides must be longer than the two transition durations combined, or the overlaps would collide.
Audio Crossfades During Transitions
A visual crossfade with a hard audio cut sounds broken. The good news: because both scenes are actually mounted during the transition window, audio placed inside each <TransitionSeries.Sequence> naturally overlaps too. All you have to do is shape the volume.
The pattern: fade each scene’s audio out over its last frames and in over its first frames, matching the transition duration. The volume prop of <Audio> accepts a callback that receives the frame relative to when the audio starts playing:
import { Audio } from "@remotion/media";
import { interpolate, staticFile } from "remotion";
const TRANSITION_FRAMES = 20;
const SceneAudio: React.FC<{
src: string;
sceneDuration: number;
}> = ({ src, sceneDuration }) => {
return (
<Audio
src={staticFile(src)}
volume={(f) =>
interpolate(
f,
[
0,
TRANSITION_FRAMES,
sceneDuration - TRANSITION_FRAMES,
sceneDuration,
],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
)
}
/>
);
};
Place a <SceneAudio> inside each sequence alongside the visuals. During the 20-frame overlap, the exiting audio ramps 1 → 0 while the entering audio ramps 0 → 1 — a clean crossfade with no extra orchestration. For the first and last scenes, skip the fade-in and fade-out respectively.
For transition sound effects — a whoosh on a slide, a tick on a clock wipe — you can attach audio to the presentation itself, so the sound automatically travels with the transition wherever you use it:
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { Audio } from "@remotion/media";
export function addSound<T extends Record<string, unknown>>(
transition: TransitionPresentation<T>,
src: string
): TransitionPresentation<T> {
const { component: Component, ...other } = transition;
const C = Component as React.FC<TransitionPresentationComponentProps<T>>;
const NewComponent: React.FC<TransitionPresentationComponentProps<T>> = (
p
) => {
return (
<>
{p.presentationDirection === "entering" ? <Audio src={src} /> : null}
<C {...p} />
</>
);
};
return { component: NewComponent, ...other };
}
// Usage:
// presentation={addSound(slide({ direction: "from-right" }), staticFile("whoosh.mp3"))}
The presentationDirection === "entering" guard is important — without it, the sound would play twice, once for each wrapped scene. And if you want a sound-only transition over a hard visual cut, combine this wrapper with the none() presentation mentioned earlier.
FAQ
Q: Can I nest a TransitionSeries inside a regular <Sequence>?
Yes. It is a normal React component and composes with <Sequence>, <AbsoluteFill>, and other primitives. A common layout: a persistent background layer with a TransitionSeries of foreground scenes on top.
Q: Do transitions affect render performance?
During the overlap window, both scenes render on every frame, so a frame inside a transition costs roughly twice as much as one outside. Negligible for typical scenes; for heavy scenes (large videos, complex SVG), keep transitions short.
Q: Can I use different timing functions for different transitions in the same series?
Yes. Each <TransitionSeries.Transition> takes its own timing and presentation — mixing a spring slide with a linear fade in one series is perfectly fine. Just include each transition’s actual duration in your length math.
Summary
@remotion/transitionsgives you<TransitionSeries>, which alternatesSequence(scenes) andTransition(effects) children- Five built-in presentations —
fade,slide,wipe,flip,clockWipe— plusnone()for invisible, timing-only overlaps - Timing is pluggable:
linearTimingfor constant or eased progress,springTimingfor physical motion - A custom presentation is ~30 lines: a component receiving
presentationProgressandpresentationDirection, wrapped in a factory function - Transitions overlap scenes, so total duration = scene durations − transition durations; compute it with
getDurationInFrames()instead of guessing - Audio crossfades fall out naturally from the overlap — shape each scene’s audio with a
volumecallback
Speeding Things Up with Ready-Made Transition Packs
Building one custom presentation is a good afternoon exercise. Building a coherent set — matched easing, consistent durations, directions that work in both horizontal and vertical formats — is real design work.
RenderComp offers production-ready Remotion transition components and multi-scene templates with tuned spring configs, TypeScript prop interfaces, and duration math already wired into calculateMetadata. Drop them into your TransitionSeries, pass your scenes as children, and render.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →