Remotion Sequence and Series: Mastering Animation Timing
Remotion Sequence and Series: Mastering Animation Timing
Every Remotion composition starts with a single, monotonically increasing number: the global frame counter. Call useCurrentFrame() anywhere in your component tree and you get that counter — starting at 0 on the first frame and climbing to durationInFrames - 1 at the end. Simple, clean, and completely predictable.
The problem arrives the moment you try to build anything with more than one moving part. Suppose you want a headline that fades in at frame 0, a subtitle that appears at frame 30, and a graphic that enters at frame 90. You could drive all three with raw frame arithmetic — opacity = frame >= 30 ? 1 : 0 — but that approach is brittle, hard to read, and a nightmare to refactor. Move the headline and you must manually update every downstream offset.
<Sequence> and <Series> exist to solve this problem elegantly. Together they form Remotion’s timing system: a declarative, composable way to describe when things happen and for how long — without hard-coding raw frame numbers throughout your components.
The Global Frame Problem
Before looking at the solution, it is worth understanding exactly what the problem looks like in practice. Here is a three-panel title card written without any timing abstractions:
import { useCurrentFrame, AbsoluteFill } from 'remotion';
export const TitleCard = () => {
const frame = useCurrentFrame();
const headlineOpacity = Math.min(1, frame / 15);
const subtitleOpacity = frame < 30 ? 0 : Math.min(1, (frame - 30) / 15);
const graphicOpacity = frame < 90 ? 0 : Math.min(1, (frame - 90) / 20);
return (
<AbsoluteFill>
<h1 style={{ opacity: headlineOpacity }}>Welcome</h1>
<h2 style={{ opacity: subtitleOpacity }}>to RenderComp</h2>
<img style={{ opacity: graphicOpacity }} src="logo.png" />
</AbsoluteFill>
);
};
This works, but every element knows about global time. If you change the headline duration from 15 to 25 frames, you must also update the subtitle and graphic offsets. More importantly, none of these elements can be extracted into a reusable <FadeIn> component, because that component would need to know its absolute start frame — which changes every time you reorder the composition.
<Sequence> fixes this by transforming the frame counter that children see.
The <Sequence> Component
<Sequence> is the fundamental building block of Remotion’s timing system. Its single most important behaviour is this: any child component that calls useCurrentFrame() inside a <Sequence> receives a frame number that is relative to the sequence’s own start, not to the global timeline.
When you write <Sequence from={30}>, children see frame 0 when global time is at frame 30. They see frame 1 when global time is 31, and so on. The global counter is never exposed; the children simply think their video started at zero.
Props
<Sequence
from={number} // frame offset from the parent timeline (default: 0)
durationInFrames={number} // how long children are mounted (default: Infinity)
layout="absolute-fill" // "absolute-fill" | "none" (default: "absolute-fill")
name="MySequence" // appears in the Remotion timeline panel (optional)
style={CSSProperties} // styles applied to the container div (optional)
premountFor={number} // pre-render children N frames before they appear (optional)
>
{children}
</Sequence>
from — The frame at which children become visible, relative to the parent timeline. Positive values delay the start. Negative values are valid and interesting: from={-10} means the child animation has already been running for 10 frames when it becomes visible — a useful trick for trimming a slow build-in.
durationInFrames — Children are unmounted when the current global frame exits the sequence’s active window. This matters for performance (no wasted calculations) and for correctness (elements do not linger on screen after their scene ends). The default of Infinity means children stay mounted for the rest of the composition.
layout — By default, <Sequence> wraps its children in an <AbsoluteFill> div. Set layout="none" if you want to position the sequence’s content yourself, or if you are using <Sequence> purely as a timing wrapper without any visual container.
name — A human-readable label that shows up in the Remotion Studio timeline panel. This has no effect on rendering, but it is invaluable when debugging a composition with a dozen nested sequences.
premountFor — Instructs Remotion to mount the children this many frames before from is reached. The children render off-screen during that window, so that expensive components (video elements, heavy SVGs) are ready the instant they appear. From Remotion v5, the default value is fps (one second of pre-mounting).
A Reusable Timing Pattern
With <Sequence>, the raw-frame example becomes:
import { Sequence, AbsoluteFill } from 'remotion';
import { FadeIn } from './FadeIn'; // a simple opacity component that reads frame 0..N
export const TitleCard = () => (
<AbsoluteFill>
<Sequence from={0} durationInFrames={60} name="Headline">
<FadeIn duration={15}><h1>Welcome</h1></FadeIn>
</Sequence>
<Sequence from={30} durationInFrames={60} name="Subtitle">
<FadeIn duration={15}><h2>to RenderComp</h2></FadeIn>
</Sequence>
<Sequence from={90} durationInFrames={Infinity} name="Graphic">
<FadeIn duration={20}><img src="logo.png" /></FadeIn>
</Sequence>
</AbsoluteFill>
);
FadeIn knows nothing about when it starts globally. It only knows how many frames its fade should take. You can reuse it for every element, in every composition, without modification. Moving an element means changing one from number in one place.
Nesting Sequences for Hierarchical Timing
<Sequence> components can be nested arbitrarily. Each nesting level shifts the frame counter by its own from offset — offsets compound, but in a clean and predictable way.
// A "scene" component with its own internal timing
const Scene = () => (
<AbsoluteFill style={{ background: '#1a1a2e' }}>
{/* Relative to whatever scene receives as frame 0 */}
<Sequence from={0} durationInFrames={30} name="Scene/TitleIn">
<TitleSlide />
</Sequence>
<Sequence from={20} durationInFrames={60} name="Scene/BodyIn">
<BodyContent />
</Sequence>
</AbsoluteFill>
);
// Top-level composition places scenes on the global timeline
export const Presentation = () => (
<AbsoluteFill>
<Sequence from={0} durationInFrames={150} name="Scene1">
<Scene />
</Sequence>
<Sequence from={150} durationInFrames={150} name="Scene2">
<Scene />
</Sequence>
</AbsoluteFill>
);
Inside Scene, everything is expressed relative to the scene’s own zero. When the global timeline is at frame 160, the second scene sees frame 10 internally, and the BodyContent inside it sees frame 0 because it starts at from={20} within a scene that itself started at from={150}.
This hierarchical model makes large compositions manageable. You can develop and test each scene in isolation — set from={0} on a composition that wraps just that scene — and then assemble the full presentation on the outer timeline.
The <Series> Component
Nesting sequences manually is powerful, but there is a specific pattern so common that Remotion ships a dedicated component for it: playing scenes one after another, back to back. This is <Series>.
Instead of calculating from offsets yourself, <Series> reads the durationInFrames of each <Series.Sequence> child and automatically positions each one immediately after the previous one ends.
import { Series, AbsoluteFill } from 'remotion';
export const Slideshow = () => (
<AbsoluteFill>
<Series>
<Series.Sequence durationInFrames={90}>
<SlideOne />
</Series.Sequence>
<Series.Sequence durationInFrames={120}>
<SlideTwo />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<SlideThree />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
Remotion places SlideOne at frames 0–89, SlideTwo at 90–209, and SlideThree at 210–269. You never wrote a single from value. Want to add a new slide between SlideOne and SlideTwo? Insert another <Series.Sequence> in the JSX and every subsequent offset updates automatically.
<Series.Sequence> Props
<Series.Sequence
durationInFrames={number} // required (except for the last child, which may use Infinity)
from={number} // offset relative to the auto-calculated start position (optional)
layout="none" // "absolute-fill" | "none" — default is "none" for Series.Sequence
premountFor={number} // pre-render this many frames before the sequence starts
>
{children}
</Series.Sequence>
The from prop on <Series.Sequence> is an additional offset on top of the auto-calculated position — not an absolute value. Setting from={-10} on SlideTwo causes it to appear 10 frames early, overlapping with the end of SlideOne. This is the correct way to create cross-fades or overlapping transitions between sequential scenes. Only the last <Series.Sequence> may have durationInFrames={Infinity}.
<Series.Sequence> vs Manual <Sequence from={...}>
<Series.Sequence> | <Sequence from={...}> | |
|---|---|---|
| Offset calculation | Automatic | Manual |
| Adding/removing scenes | Just insert/delete JSX | Must update all downstream from values |
| Overlapping scenes | from as relative offset | Full manual control |
| Non-sequential layout | Not suited | Full control |
| Best for | Slideshows, step-by-step sequences | Complex compositions with parallel elements |
The practical rule: if scenes follow one another in a strict sequence, use <Series>. If scenes overlap, run in parallel, or have complex positional relationships, use <Sequence> with explicit from values.
Practical Example: Multi-Slide Presentation
Let us build a complete three-slide presentation that combines both components. Each slide has its own internal <Sequence> structure for title and body animations; the slides themselves are managed by <Series>.
import { Series, Sequence, AbsoluteFill, useCurrentFrame } from 'remotion';
import { spring, useVideoConfig } from 'remotion';
// A slide template with internal timing
const Slide = ({
title,
body,
bg,
}: {
title: string;
body: string;
bg: string;
}) => (
<AbsoluteFill style={{ background: bg, padding: '80px' }}>
{/* Title fades and slides in over the first 20 frames */}
<Sequence from={0} durationInFrames={Infinity} name="SlideTitle">
<TitleEntry text={title} />
</Sequence>
{/* Body content enters 20 frames after the title */}
<Sequence from={20} durationInFrames={Infinity} name="SlideBody">
<BodyEntry text={body} />
</Sequence>
</AbsoluteFill>
);
// Entry animation using spring — reads frame starting from 0 thanks to Sequence
const TitleEntry = ({ text }: { text: string }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, config: { damping: 12, stiffness: 80 } });
return (
<h1
style={{
opacity: progress,
transform: `translateY(${(1 - progress) * 40}px)`,
fontSize: 72,
color: '#fff',
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
}}
>
{text}
</h1>
);
};
const BodyEntry = ({ text }: { text: string }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, config: { damping: 14, stiffness: 70 } });
return (
<p
style={{
opacity: progress,
transform: `translateY(${(1 - progress) * 24}px)`,
fontSize: 36,
color: 'rgba(255,255,255,0.85)',
marginTop: 24,
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
}}
>
{text}
</p>
);
};
// Top-level composition — Series handles all the sequential positioning
export const Presentation = () => (
<AbsoluteFill style={{ background: '#0f0f23' }}>
<Series>
<Series.Sequence durationInFrames={120}>
<Slide title="The Problem" body="Global frames make timing complex." bg="#1a1a2e" />
</Series.Sequence>
<Series.Sequence durationInFrames={120}>
<Slide title="The Solution" body="Sequence shifts the frame counter." bg="#16213e" />
</Series.Sequence>
<Series.Sequence durationInFrames={Infinity}>
<Slide title="The Result" body="Clean, reusable, composable animations." bg="#0f3460" />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
Notice that TitleEntry and BodyEntry are completely ignorant of the global timeline. Each one starts from frame 0. The <Sequence from={20}> that wraps BodyEntry ensures it begins its animation 20 frames into whichever slide it is placed in — and the <Series> above ensures each slide follows the previous one seamlessly.
Combining <Sequence> with Spring Animations
Spring animations and <Sequence> are natural partners. A spring in Remotion runs from frame 0 — and <Sequence> ensures that frame 0 is exactly when you want the spring to start.
The key insight is that spring({ frame, fps }) always starts at frame 0. Without <Sequence>, you must write spring({ frame: frame - startFrame, fps }) everywhere — a manual offset that is fragile and easy to get wrong. With <Sequence>, the frame counter inside the sequence is already zeroed for you:
// Without Sequence — fragile
const opacity = spring({ frame: frame - 90, fps });
// With Sequence — clean
// (inside a <Sequence from={90}>)
const opacity = spring({ frame, fps });
The spring example in the presentation above uses exactly this pattern. TitleEntry calls spring({ frame, fps }) where frame is the sequence-local frame — always starting at 0 when the slide first appears.
You can also delay the spring further within a sequence by subtracting a small value from the frame:
// Spring that starts 5 frames into the sequence
const progress = spring({ frame: Math.max(0, frame - 5), fps });
Using Math.max(0, frame - 5) clamps negative frames to zero — the spring stays at its initial value until frame 5 and then runs normally. This is far cleaner than tracking absolute start times.
FAQ
Q: What is the difference between <Sequence> and <AbsoluteFill>?
<AbsoluteFill> is a layout primitive — it positions its children to fill the composition with position: absolute; inset: 0. <Sequence> is a timing primitive — it controls when children appear and shifts the frame counter they receive. By default, <Sequence> renders its own <AbsoluteFill> container (the layout="absolute-fill" default), but their purposes are entirely distinct. You can set layout="none" on a <Sequence> if you do not want the positional wrapper.
Q: Can I use <Sequence> outside of a composition’s root?
Yes. <Sequence> works at any depth in the component tree. You can nest sequences inside other sequences, inside custom components, even inside mapped arrays. The frame counter shift compounds correctly through every level of nesting.
Q: What happens if from is larger than the composition’s durationInFrames?
The children are simply never mounted. Remotion does not throw an error — the sequence’s time window never intersects with the rendered range. This is expected behaviour and can be useful for conditional content that you want to skip in shorter previews.
Q: Can <Series.Sequence> contain <Sequence> children?
Absolutely. <Series.Sequence> is a <Sequence> under the hood. Any component you place inside it can use <Sequence> freely for internal timing. The <Series> / <Sequence> split is purely about whether sequential auto-positioning is appropriate at that level of the hierarchy.
Q: How do I pause between slides in a <Series>?
The cleanest approach is to insert a pause sequence explicitly:
<Series>
<Series.Sequence durationInFrames={90}>
<SlideOne />
</Series.Sequence>
{/* 30-frame pause — render a blank or hold the previous state */}
<Series.Sequence durationInFrames={30}>
<HoldFrame />
</Series.Sequence>
<Series.Sequence durationInFrames={90}>
<SlideTwo />
</Series.Sequence>
</Series>
Alternatively, give SlideOne a longer durationInFrames than its animation actually needs — the extra frames simply hold the final frame of the animation.
Q: Does durationInFrames on <Sequence> affect the total composition length?
No. <Sequence> only controls when its children are mounted and what frame value they receive. The composition’s total duration is always determined by the durationInFrames prop on <Composition> in your root file. Think of <Sequence> as a viewport on the timeline — it does not extend or shorten the timeline itself.
Q: Is there a performance cost to nesting many sequences?
Sequences are very lightweight. The main cost is the DOM element generated by layout="absolute-fill" (a single div). For deeply nested compositions with dozens of sequences, consider using layout="none" where you do not need the positioning wrapper, and rely on premountFor to pre-warm expensive children before they appear.
Build with RenderComp Templates
Understanding <Sequence> and <Series> unlocks the full power of programmatic video. Every RenderComp template is built with these primitives — each scene is a <Sequence> with clean internal timing, each template is a <Series> that assembles scenes in order.
When you open a RenderComp template, you will find from values that are easy to understand, <Series> wrappers that make slide ordering effortless, and spring animations that start from local frame 0 every time. The result is a codebase you can read, modify, and extend — not a tangled web of hard-coded frame arithmetic.
Browse the RenderComp template library at rendercomp.com and see how professional-grade timing is done in practice.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →