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.
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.
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
fromvalues 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
- The Parallax component uses invalidateOnRefresh so the per-layer transform endpoints recompute when the section height changes
- The Multi Flip uses invalidateOnRefresh on the scrub timeline so the per-card scroll positions stay accurate across resizes
- The Popping Text uses invalidateOnRefresh after SplitText runs - font load can shift line breaks, and the per-character from positions need to follow
- The Image Dissolve Scroll uses invalidateOnRefresh on the pin timeline so the dissolve start position survives image lazy-load reflow
See it running in production
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()(notgsap.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()andgsap.fromTo()cache at creation, which is when staleness can bite. If yourtotween 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.matchMediais 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. Wiredocument.fonts.ready.then(() => ScrollTrigger.refresh())at app boot.
