Pattern · 24
Timeline
时间线 · timelinePut several tweens on one clock and specify which follow, overlap or align.
With separate timeouts, changing one duration can leave every later start time wrong. A timeline calculates each segment's start and end against one clock. Overlaps become visible and a single playhead can scrub the whole sequence.
Formula
> start at previous end < start at previous start +=.2 start .2 s after previous end <.1 start .1 s after previous start cue start at the named label
Parameters
| This page | Mature libraries | |
|---|---|---|
| position | editable per segment | GSAP accepts <, >, relative offsets and labels |
| duration | 0.55–0.8 s | —The five demo segments' durations. |
| playhead | draggable | — |
Doing it well
- Use relative positions for “after the last segment” and “start together” instead of hard-coding every absolute second. GSAP resolves these against the recent segment's start or end.GSAP src/gsap-core.js:390–413
- Name an alignment point. When earlier durations change, segments placed at
cuestill follow that label.GSAP src/gsap-core.js:403–405 - Overlap staggered entrances instead of waiting for each part to finish. In
nova, background, title and copy start at 0, 0.3 and 0.42 seconds.www-motion/rewrite/nova/app.js:10 - Kill an old timeline when replaying or closing a view so its callbacks do not overwrite the new state;
formationstops the previous timeline on close.www-motion/rewrite/formation/shutter.js:4–7
In our cases
44 studies use timelines. nova overlaps background, title, copy and cards at different starts; formation sequences the stages of opening and closing a detail view. One playhead makes those relationships easier to inspect.
Used in 44 cases
Reference implementation · basics.js
/**
* Lay out tweens on one clock, GSAP position-parameter style:
* number absolute time '<' same start as previous '>' (default) after previous
* '+=0.2' / '-=0.2' relative to the end of previous '<0.1' relative to previous start 'label'
*/
export function timeline(items) {
const labels = {}; let prevStart = 0, prevEnd = 0;
const placed = items.map(item => {
if (item.label && item.duration == null) { labels[item.label] = prevEnd; return null; }
const p = item.at ?? '>';
let start;
if (typeof p === 'number') start = p;
else if (p in labels) start = labels[p];
else if (p === '<') start = prevStart;
else if (p === '>') start = prevEnd;
else if (p.startsWith('<')) start = prevStart + parseFloat(p.slice(1));
else if (p.startsWith('+=') || p.startsWith('-=')) start = prevEnd + parseFloat(p.replace('=', ''));
else start = prevEnd;
prevStart = start; prevEnd = start + item.duration;
return { ...item, start, end: prevEnd };
}).filter(Boolean);
const duration = Math.max(0, ...placed.map(p => p.end));
return { items: placed, labels, duration,
progressAt: (t, item) => (item.ease || (x => x))(clamp((t - item.start) / item.duration)) };
}