Shots in this scene
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.
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
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 endand the dot is at its end position without moving. - 2
Count the leak
In "Remove a section": Mount (no cleanup), Remove, Mount (context), Remove. The four lines read
ScrollTriggers: 1 | … 0, then1 | … 2, then2 | … 2, then1 | … 2. The first mount's trigger and its endless tween are never cleaned; the second pair is.
Code & diagrams
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`.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
full motion, narrow screen: short travelClick Mount (no cleanup), Remove, then Mount (context) and Remove, reading the count each time. Scroll the little box to see the scrubbed tween.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
start → ScrollTriggers: 0 | tweens still running on removed elements: 0Break 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).
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.addconditions are evaluated withwindow.matchMedia, so they react to the device's setting live. They do not know about your owndata-motionswitch, which is why the build also callsmotionAllowed()and rebuilds ononMotionChange.
Remember this
- 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
**
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 getscontext.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
Reduced motion with matchMedia. The calm branch must leave the finished state: what a
from()withautoAlpha: 0would hide must stay visible and in place. The simplest way is to create the tweens only in theno-preferencebranch. The Phase 7 toggle can disagree with the device, so the build also asksmotionAllowed(). - 4
**
gsap.context(fn, scope).** Every tween, timeline and ScrollTrigger created whilefnruns is recorded.ctx.revert()kills them and restores the inline styles. Ascopeelement limits string selectors insidefnto 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
**
kill()versusrevert().**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
How do you give reduced-motion users a calm version with GSAP, and what must it leave in place?
What is the difference between kill() and revert(), and which would you use when a component unmounts?
Practice
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.
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
matchMediaor acontextYou can say what
revert()does thatkill()does notYou pass element references for anything outside a context's scope