Plugins

gsap.matchMedia()

gsap.matchMedia() is the GSAP API for running different animation code at different breakpoints, and for cleanly disposing of breakpoint-specific animations when the media query stops matching. It's also the canonical place to handle `prefers-reduced-motion`. Animations inside a matchMedia branch auto-revert when the query no longer matches - no manual cleanup.

Updated June 2, 2026

Mechanics

How matchMedia works

Create a matchMedia instance, then add branches keyed by media query strings. Each branch's callback runs when its query matches. GSAP tracks every tween / ScrollTrigger / timeline created inside the callback and reverts them when the query stops matching - no manual .kill() calls needed.

script.js

When the viewport crosses 767px the desktop branch reverts (the ScrollTrigger unpins, the cached positions clear) and the mobile branch fires fresh. Cross back, mobile reverts and desktop fires fresh. No leaked triggers, no stale measurements.

When

Use matchMedia for

  • Any animation that should run only at a specific breakpoint (desktop pinning, mobile-only swipe)
  • Honoring prefers-reduced-motion (the canonical place to gate the reduce branch)
  • Hover-vs-touch differentiation via (hover: hover) / (pointer: coarse) media queries
  • Light/dark mode-specific motion (rare, but supported via (prefers-color-scheme: dark))
  • Any animation whose structural shape changes between breakpoints, not just the values
Alternatives

Use something else when

  • The animation is identical at all breakpoints with only value differences (use ScrollTrigger's invalidateOnRefresh, not matchMedia)
  • You only need a one-time check at page load - window.matchMedia directly is fine for non-animation logic
  • The values are CSS-only (no JS) - prefer CSS media queries on the class itself
  • Single-tween scaling responses (use a function value: x: () => window.innerWidth * 0.5) - matchMedia ceremony isn't worth it
In production

Used in these Annnimate components

  • The Parallax component uses matchMedia to enable pinning above 768px and switch to a static stack below - mobile pinning clashes with touch momentum scroll
  • The Multi Flip uses matchMedia to swap the horizontal scrub for a vertical tap-to-advance on mobile
  • The Mega Menu uses matchMedia with (hover: hover) to enable hover-open on desktop and click-only on touch devices
  • Every Annnimate component uses matchMedia for the (prefers-reduced-motion: reduce) branch that scales global timeline to 20x (effectively instant)
Used in components

See it running in production

FAQ

Common questions

Do I need to call matchMedia inside useGSAP for React?
Yes. Create the matchMedia instance inside useGSAP, and return a cleanup that calls mm.revert(). Without it, the matchMedia instance leaks across re-renders and StrictMode double-mounts pile up duplicate branches.
What's the difference between matchMedia and window.matchMedia?
window.matchMedia is the browser API - it reports whether a query matches, fires a change event, and that's it. gsap.matchMedia wraps it AND tracks the animations created inside the callback, reverting them when the query stops matching. Use gsap.matchMedia for animations; window.matchMedia for non-animation gates.
Can I share state across matchMedia branches?
Yes - any variable declared outside the branches is shared. But avoid mutating shared state from inside the branches in ways that depend on order; the branches can fire and re-fire as queries match and unmatch.
How do I add prefers-reduced-motion correctly?
mm.add('(prefers-reduced-motion: reduce)', () => gsap.globalTimeline.timeScale(20)). This is the canonical approach - everything plays 20x speed (effectively instant). Alternative: gate every animation behind a non-reduce branch (mm.add('(prefers-reduced-motion: no-preference)', () => {...})). The timeScale trick is shorter and the right default.
What happens if matchMedia branches overlap?
All matching branches fire. A (min-width: 768px) branch and a (min-width: 1024px) branch both run at 1280px viewport - they're not mutually exclusive. Author the branches with mutually-exclusive queries ((min-width: 768px) and (max-width: 1023px)) or accept the overlap and design the branches to compose.