invalidateOnRefresh

invalidateOnRefresh is a ScrollTrigger option that tells GSAP to re-read each tween's starting values on every `ScrollTrigger.refresh()`. Without it, ScrollTrigger reuses the cached `from` values - which become stale after a layout shift, font load, or dynamic content change. The classic 'why did my pinned section jump after resize' bug is fixed by toggling this on.

Ships in
ScrollTrigger plugin
Used in
5 Annnimate components

Updated June 2, 2026

How invalidateOnRefresh works

When you create a tween with gsap.from() or gsap.fromTo(), GSAP records the element's current values at tween creation time - those become the 'from' state. ScrollTrigger caches its start/end scroll positions at trigger creation time. After a resize, font load, or dynamic content swap, BOTH caches go stale. ScrollTrigger.refresh() recomputes the start/end positions but does NOT re-read the tween's from-values - unless you opt in with invalidateOnRefresh: true.

script.js

On refresh, ScrollTrigger calls the tween's invalidate() method, which clears the cached values. The next tween playback re-reads them. This is what makes the tween survive a font load (which shifts text baselines) or an image load (which shifts layout below).

Use invalidateOnRefresh for

  • Pinned sections that contain content sized by the viewport - resize changes the pin distance
  • Stagger reveals across grids that reflow on resize
  • Text reveals with SplitText - font load changes line breaks, invalidating cached positions
  • Image-heavy layouts where late-loading images shift the document above the trigger
  • Anything where the 'from' values depend on measured layout

Use something else when

  • The tween's from values are constant (y: 60, opacity: 0) and never depend on layout - the option is unnecessary
  • You can rebuild the tween entirely on resize via gsap.matchMedia - cleaner than per-tween invalidation when the breakpoint matters
  • The bug isn't the from-values but the start/end positions - that's ScrollTrigger.refresh() alone, no invalidation needed

Used in these Annnimate components

Common questions

What's the difference between invalidateOnRefresh and ScrollTrigger.refresh()?
ScrollTrigger.refresh() recomputes each trigger's start/end positions against the current layout. invalidateOnRefresh: true tells refresh to ALSO clear the tween's cached from-values. Without it, the trigger's positions update but the tween's starting state stays frozen at the original measurements.
Do I need it when using gsap.to() (not gsap.from())?
Less often. gsap.to() reads the FROM values at playback time (the element's current state), not at creation time, so layout shifts before playback don't matter. gsap.from() and gsap.fromTo() cache at creation, which is when staleness can bite. If your to tween has computed END values (a function ref), then yes - invalidate triggers a re-read.
Does invalidateOnRefresh cause performance issues?
Negligible. refresh() runs on resize (debounced), font load, and explicit calls - not on every scroll tick. The cost is one extra tween invalidation per refresh, which is microseconds. There's no per-frame cost.
When should I rebuild the ScrollTrigger instead of using invalidateOnRefresh?
When the structural shape of the animation changes between breakpoints - e.g. a desktop horizontal scroll becomes a vertical stack on mobile. Then gsap.matchMedia is the right tool: each breakpoint creates its own ScrollTrigger which auto-disposes when the media query stops matching. invalidateOnRefresh is for when the shape stays the same but the measurements change.
Why isn't my pinned section refreshing on font load?
Two reasons. (1) You're missing invalidateOnRefresh: true on the pinned trigger - the start position recomputes but the inner tween's from-values don't. (2) You're not calling refresh() on document.fonts.ready - GSAP doesn't watch font load events. Wire document.fonts.ready.then(() => ScrollTrigger.refresh()) at app boot.