Fixing TextScrollSection Flicker at Extreme Scale (GSAP ScrollTrigger)

A practical fix for one-frame flashes/white blocks when scaling a pinned full-screen section during long reverse scroll: cap the scale, contain painting, and stabilize pin behavior.

January 27, 2026 (4w ago)
8 min read
0 views
0 likes
Category
Intermediate
#gsap#scrolltrigger#nextjs#animation#performance#compositing#pin

Fixing TextScrollSection Flicker at Extreme Scale (GSAP ScrollTrigger)

I ran into a nasty visual bug in a scroll-driven “sticky text” scene: after scrolling deep down the page once and then reverse-scrolling back, the third screen’s background/text would occasionally flash, show white blocks, or briefly disappear at the moment the text was at its maximum stretch.

This post documents the root cause and a robust fix that keeps the vibe of the animation while eliminating the compositor instability.

Symptoms

  • The glitch was easiest to reproduce with the following flow:
    1. Scroll to near the bottom of the page (long distance).
    2. Reverse scroll back into the third “sticky text” screen.
    3. The flicker starts around the point where the third screen is close to its maximum scale.
  • The flicker looked like:
    • one-frame background flash,
    • random white blocks,
    • text/background popping out and back.

Root cause (why it flickers)

The original implementation scaled the entire full-screen container (including gradient background + background image + huge text) to a very large factor (e.g. 10×).

When a pinned element is transformed into a massive surface, browsers may:

  • promote/demote the element’s layer,
  • re-tile and re-rasterize it,
  • aggressively reclaim GPU memory after long scroll distance,
  • and rebuild the surface on reverse scroll.

At the extreme scale (and especially with pin + scrub), these layer transitions can produce a 1-frame hole (flash) or partial tiles (white blocks).

Fix strategy

Instead of fighting the browser with more “force GPU” hints, the most reliable approach was to reduce the pathological case:

  1. Cap the maximum scale of the full-screen container
    • Reducing from 10× to ~3× avoids huge surfaces and keeps raster tiles in a stable range.
  2. Start the fade transitions earlier to keep the same drama
    • If the container can’t go to 10×, begin fading the gradient/text earlier so the overall “disappear while expanding” feel stays.
  3. Contain the painted region
    • Apply contain: layout style paint to limit the impact of painting/compositing work.
  4. Stabilize multi-pin pages (order matters)
    • Create pinned ScrollTriggers in useLayoutEffect so layout/spacers are applied before calculating start/end.
    • Use refreshPriority to ensure upper pinned sections refresh before lower ones (Cosmic/Goals), preventing “start/end not moving” style bugs.

Implementation notes

The fix lives in:

  • src/components/ui/text-scroll-section.tsx

Key changes:

  • The third screen’s scale-up stage uses MAX_SCALE = 3 (instead of 10).
  • Stage timings were re-mapped to keep the narrative:
    • Scale up: 0–60%
    • Gradient fade: 20–45%
    • Text fade: 40–65%
    • Header reveal: 70–90%
  • contain: layout style paint is applied to the scaled container.
  • The third screen’s pinned ScrollTrigger is created in useLayoutEffect and given a higher refreshPriority so downstream pinned sections don’t miscalculate their start/end.

Takeaways

  • Scaling a pinned, full-viewport element to extreme factors is a common way to hit compositor edge cases.
  • If you see flicker only after long scroll distance + reverse scroll, suspect layer recycling.
  • The “best fix” is often to avoid creating a gigantic surface, then compensate the visuals with earlier fades and better anchoring.
CleanLove

Written by CleanLove

Frontend engineer exploring delightful motion and robust UX