Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 14.5Advanced ~19 min · shot 5 of 7

matchMedia, Reduced Motion and Cleanup

In one line

gsap.matchMedia() runs animation code only while a media query matches and undoes it when it stops matching. gsap.context() collects everything GSAP creates so one revert() removes it all.

0/7 · 0%

Think of it like this

A rented stage set. The crew builds the set for tonight's show and, at the end, strikes it: every flat, every light goes back to how the room was. A crew that only builds leaves the next show standing among the old scenery. A GSAP animation that is created and never undone is that crew.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

matchMedia
A GSAP helper that runs animation code only while a media query matches and reverts it afterwards.
context
A recorder: everything GSAP creates inside it can be reverted or killed with one call.
revert
Kill the animations and restore the inline styles they wrote.
scope
The element a context limits its string selectors to.

Step by step

01Read which branch ran

In "One function, three conditions" the console says which branch ran. In the checker's 300 px-wide frame that is full motion, narrow screen: short travel. Switch Reduced motion on in the timeline: the page reruns as a reduced-motion user, the dot sits at its end and the track list is empty.

02Stop watching

Press Stop watching. mm.revert() runs the cleanup function, which prints cleanup ran, kills the tween and puts the dot back at its CSS position (transform: none). Nothing listens any more.

03Leak a section, then clean one

In "Remove a section" press Mount (no cleanup) then Remove section. The console says ScrollTriggers: 1 | tweens still running on removed elements: 2: the endless rotation and the scrubbed tween outlive the section. A context mount leaves nothing behind (Count the leak, below).

04Reach outside the scope

Inside gsap.context(build, host) a string selector is searched only in host. A scroller: "#scroller" there prints Element not found: #scroller, because the scroller is the parent. Pass an element reference.

Try it yourself

  1. 1

    Switch reduced motion on

    In "One function, three conditions" use the timeline's Reduced motion switch. The console line becomes calm: the dot just sits at its end and the dot is at its end position without moving.

  2. 2

    Count the leak

    In "Remove a section": Mount (no cleanup), Remove, Mount (context), Remove. The four lines read ScrollTriggers: 1 | … 0, then 1 | … 2, then 2 | … 2, then 1 | … 2. The first mount's trigger and its endless tween are never cleaned; the second pair is.

Code & diagrams

One function, three conditions

Named conditions give one function every case. The checker frame is 300 px wide, so `narrow` is true. The line under the dot follows the viewfinder's width; when it changes, the console prints `cleanup ran`.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

full motion, narrow screen: short travel
Remove a section: leak or revert

Click Mount (no cleanup), Remove, then Mount (context) and Remove, reading the count each time. Scroll the little box to see the scrubbed tween.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

start → ScrollTriggers: 0 | tweens still running on removed elements: 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use a selector outside the context's scope

In "Remove a section" change scroller: document.getElementById("scroller") to scroller: "#scroller" and press Mount (context).

terminal
$ Read the console
── what you'll see ──
Element not found: #scroller (a console warning)
Uncaught TypeError: Cannot read properties of undefined (reading '_gsap')

Myth vs fact

Myth

Removing the element from the page stops its animation.

Fact

GSAP holds a reference to the element. The tween keeps running on the detached node and keeps memory alive until it is killed or reverted.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    mm.add conditions are evaluated with window.matchMedia, so they react to the device's setting live. They do not know about your own data-motion switch, which is why the build also calls motionAllowed() and rebuilds on onMotionChange.

Remember this

  1. 1

    The problem this solves: an animation must follow the visitor. A phone should get shorter travel than a desktop, a visitor who asked for less motion should get none, and both can change while the page is open (rotate the phone, flip the setting). And when a component leaves the page, its tweens and ScrollTriggers must go with it, or they keep running on elements nobody can see.

  2. 2

    **gsap.matchMedia().** mm.add("(prefers-reduced-motion: no-preference)", () => { … }) runs the function while the query matches. Pass an object of named queries instead and the function gets context.conditions ({ calm, full, narrow }), so one function can serve all cases. Everything you create inside is reverted when the match stops, and made again when it starts. mm.revert() removes all of it and stops listening.

  3. 3

    Reduced motion with matchMedia. The calm branch must leave the finished state: what a from() with autoAlpha: 0 would hide must stay visible and in place. The simplest way is to create the tweens only in the no-preference branch. The Phase 7 toggle can disagree with the device, so the build also asks motionAllowed().

  4. 4

    **gsap.context(fn, scope).** Every tween, timeline and ScrollTrigger created while fn runs is recorded. ctx.revert() kills them and restores the inline styles. A scope element limits string selectors inside fn to its descendants, so a component can animate ".dot" without touching other components' dots. ctx.add(fn) runs later code, such as an event handler, inside the same context.

  5. 5

    **kill() versus revert().** kill() stops a tween where it is. revert() also undoes what it wrote: use it when the element must look untouched, for example when reduced motion switches on half way.

Explain it without notes

01

How do you give reduced-motion users a calm version with GSAP, and what must it leave in place?

02

What is the difference between kill() and revert(), and which would you use when a component unmounts?

Practice

01

Make a small init(section) function that animates a section's children with a context scoped to the section, and returns a function that removes the animation completely.

02

Write a matchMedia that gives desktop (min-width 800px) a long slide and mobile a short one, and nothing at all for reduced motion.

Done when you can

  • You build GSAP animation inside matchMedia or a context

  • You can say what revert() does that kill() does not

  • You pass element references for anything outside a context's scope