Skip to content

Timeline: <Sequence>, <Series>, <Loop>

These time-shifting primitives are how you assemble a timeline — Remotion’s compositional grammar. They work by manipulating the frame context: children inside them see a shifted useCurrentFrame(), and render only within their window.

Shifts children in time. Inside a <Sequence from={N}>, useCurrentFrame() returns outerFrame - N, and the children render only while 0 <= localFrame < durationInFrames.

interface SequenceProps {
from?: number // frame this sequence starts (children's frame 0). Default 0.
durationInFrames?: number // how long it lasts; unbounded if omitted.
children?: ReactNode
}
import { Sequence, Text, interpolate, useCurrentFrame } from 'onda-engine/react'
function Title() {
const frame = useCurrentFrame() // 0 at the sequence's start
const opacity = interpolate(frame, [0, 15], [0, 1])
return <Text opacity={opacity} fontSize={96} color="#fff">Hello</Text>
}
// Title appears at frame 30 and lasts 60 frames.
<Sequence from={30} durationInFrames={60}>
<Title />
</Sequence>

Outside its window the <Sequence> renders null (nothing).

Plays <Series.Sequence> children back-to-back: each starts where the previous ended (cumulative offsets from their durationInFrames). You don’t compute from yourself.

interface SeriesSequenceProps {
durationInFrames: number // required
children?: ReactNode
}
import { Series } from 'onda-engine/react'
<Series>
<Series.Sequence durationInFrames={30}>
<Intro />
</Series.Sequence>
<Series.Sequence durationInFrames={45}>
<Body />
</Series.Sequence>
<Series.Sequence durationInFrames={30}>
<Outro />
</Series.Sequence>
</Series>

Here <Intro> plays on frames 0–29, <Body> on 30–74, <Outro> on 75–104. Each child sees its own local frame 0.

Repeats children forever: inside, useCurrentFrame() returns frame % durationInFrames.

interface LoopProps {
durationInFrames: number // length of one iteration
children?: ReactNode
}
import { Loop, Rect, interpolate, useCurrentFrame } from 'onda-engine/react'
function Pulse() {
const frame = useCurrentFrame() // cycles 0..29
const opacity = interpolate(frame, [0, 15, 30], [1, 0.3, 1])
return <Rect opacity={opacity} width={100} height={100} fill="#22d3ee" />
}
<Loop durationInFrames={30}>
<Pulse />
</Loop>

durationInFrames <= 0 renders nothing.

Like <Series>, but consecutive sequences overlap by a transition’s duration — cross-fading (or sliding/wiping) from one to the next. Children alternate <TransitionSeries.Sequence durationInFrames={…}> and <TransitionSeries.Transition presentation={…} timing={…} />.

Switch the presentation and watch it run (rendered live by the engine):

import {
TransitionSeries,
fade,
slide,
wipe,
linearTiming,
springTiming,
} from 'onda-engine/react'
;<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<Intro />
</TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({ durationInFrames: 20 })} />
<TransitionSeries.Sequence durationInFrames={60}>
<Main />
</TransitionSeries.Sequence>
</TransitionSeries>

Presentations: fade() (opacity), slide({ direction }), wipe({ direction }), where direction is 'from-left' | 'from-right' | 'from-top' | 'from-bottom'. Timings: linearTiming({ durationInFrames }), springTiming({ durationInFrames?, config? }). A transition’s duration is the overlap between its two sequences, so the total length is the sum of the sequence durations minus the transition durations. Built entirely on opacity / translate / clip, so it works on both backends and in the browser.

All three (plus <TransitionSeries>) are React components that re-provide the internal frame context with a shifted frame value (and, for <Sequence>, conditionally render based on visibility). Because everything downstream reads the frame through that context, the shift composes naturally — nest a <Loop> inside a <Sequence> and the offsets stack.

Remotion’s <Freeze> and a declarative Rust-side equivalent are noted as TODOs and are not implemented today.