Scroll reveals that don't get in the way
Why 12px beats 60px, why `once: true` matters more than the easing, and the one case where a reveal actively costs you money.
Scroll reveals are the first animation everyone adds and the first one that makes a site feel cheap. Three rules have survived every project I've used them on.
Small offsets read as intent, large ones read as lag
A 60px slide takes long enough that a fast scroller sees the element mid-flight and reads it as the page struggling. 8–16px reads as a fade with a hint of direction.
gsap.from(el, {
opacity: 0,
y: 12,
duration: 0.35,
ease: "power1.out",
scrollTrigger: { trigger: el, start: "top 90%", once: true },
});once: true unless you have a reason
Re-triggering on scroll-up means someone reviewing your page — which is exactly what a prospective client does — watches everything animate a second and third time. It stops being a reveal and becomes a tax on re-reading.
Never reveal something a crawler needs
Anything set to opacity: 0 by CSS and revealed by JS is invisible if the script fails. For a portfolio, that's your entire case-study copy.
The fix is one line: put a no-js class on <html>, remove it in an inline script before paint, and scope the hidden state to .no-js being absent. If JS never arrives, everything is simply visible.
<html class="no-js">
<script>document.documentElement.classList.remove('no-js')</script>.no-js .anim-hidden { opacity: 1; }And respect the setting
prefers-reduced-motion isn't a preference for less animation, it's a medical accommodation for vestibular disorders. Don't shorten the duration — skip the transform entirely and set the end state.
- gsap
- scrolltrigger
- ux