Tweens & timelines

GSAP animates any property of anything, and sequences it on a timeline you can control like a video.

What it is

A tween (gsap.to(), from(), fromTo()) animates properties from one value to another: positions, colours, SVG attributes, even plain JavaScript numbers. A timeline chains tweens, with the position parameter ("<0.15", "-=0.3") controlling overlaps, and can be played, paused, reversed or scrubbed as one unit. In React, useGSAP() from @gsap/react scopes selectors to the component and cleans everything up when it unmounts.

When to use it

  • Intro and transition sequences with precise timing
  • Anything that should be scrubbable or reversible
  • Animating SVG, canvas or 3D values, not just CSS
Official docs ↗

A timeline you can scrub

Three tweens, staggered with the position parameter, then a colour change that overlaps the end. Try different eases.

0%
Show code · src/app/gsap-basics/TimelineDemo.tsx
"use client";

import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { useRef, useState } from "react";
import { useMotion } from "@/components/motion/MotionProvider";
import { Button, Stage } from "@/components/ui";

gsap.registerPlugin(useGSAP);

const EASES = ["power2.inOut", "back.out(1.7)", "elastic.out(1, 0.4)", "bounce.out", "none"];

// A timeline: three tweens in sequence, controlled like a video. The slider
// both shows and sets the timeline's progress (0 → 1).
export function TimelineDemo() {
  const { reduced } = useMotion();
  const stage = useRef<HTMLDivElement>(null);
  const tl = useRef<gsap.core.Timeline | null>(null);
  const [ease, setEase] = useState(EASES[0]);
  const [progress, setProgress] = useState(0);

  useGSAP(() => {
    const travel = (stage.current?.clientWidth ?? 400) - 120;
    tl.current = gsap
      .timeline({ paused: reduced, repeat: reduced ? 0 : -1, repeatDelay: 0.6, yoyo: true, defaults: { duration: 0.9, ease }, onUpdate: () => setProgress(tl.current?.progress() ?? 0) })
      // In reduced mode the same sequence runs, but with no travel: only
      // colour and opacity change.
      .to(".a", reduced ? { opacity: 0.3 } : { x: travel })
      .to(".b", reduced ? { opacity: 0.3 } : { x: travel, rotation: 180 }, "<0.15") // starts 0.15 s after the previous one starts
      .to(".c", reduced ? { opacity: 0.3 } : { x: travel, scale: 0.6, borderRadius: "50%" }, "<0.15")
      .to(".box", { backgroundColor: "#e0577a", duration: 0.5 }, "-=0.3"); // overlaps the end
  }, { scope: stage, dependencies: [ease, reduced], revertOnUpdate: true });

  return (
    <>
      <Stage ref={stage} className="grid h-58 content-evenly px-7">
        <div className="box a size-16 rounded-xl bg-accent" />
        <div className="box b size-16 rounded-xl bg-accent" />
        <div className="box c size-16 rounded-xl bg-accent" />
      </Stage>
      <div className="mb-2 flex flex-wrap items-center gap-2">
        <Button onClick={() => tl.current?.play()}>Play</Button>
        <Button variant="ghost" onClick={() => tl.current?.pause()}>Pause</Button>
        <Button variant="ghost" onClick={() => tl.current?.reverse()}>Reverse</Button>
        <Button variant="ghost" onClick={() => tl.current?.restart()}>Restart</Button>
        <label className="text-sm">Ease{" "}
          <select value={ease} onChange={(e) => setEase(e.target.value)}>
            {EASES.map((e) => <option key={e}>{e}</option>)}
          </select>
        </label>
      </div>
      <div className="mb-2 flex flex-wrap items-center gap-2">
        <input
          className="min-w-0 flex-1"
          type="range" min={0} max={1} step={0.001} value={progress} aria-label="Timeline progress"
          onChange={(e) => tl.current?.pause().progress(Number(e.target.value))}
        />
        <span className="text-sm tabular-nums">{Math.round(progress * 100)}%</span>
      </div>
      {reduced && <p className="text-sm text-muted">Reduced motion: the timeline doesn&apos;t autoplay and only fades. Press Play, or switch to Full in the sidebar.</p>}
    </>
  );
}