@vettvangur/scroll-trigger
v0.0.6
Published
Vettvangur | Declarative GSAP ScrollTrigger helper
Maintainers
Keywords
Readme
@vettvangur/scroll-trigger
Declarative scroll-driven animations on top of GSAP ScrollTrigger.
Drop a data-scroll attribute on a section, mark items with data-scroll-item, and a sensible fade-up runs as the section enters the viewport. Custom section choreography is registered as a named preset.
Install
pnpm add @vettvangur/scroll-trigger gsapgsap is a peer dependency.
Quick start
<section data-scroll>
<h2 data-scroll-item>A heading</h2>
<p data-scroll-item>Some body text</p>
</section>import { initScrollTrigger } from '@vettvangur/scroll-trigger'
const cleanup = initScrollTrigger()That's it — every [data-scroll] section gets a fade-up animation that plays once when its top crosses 80% of the viewport.
Full example — default + hero + banner
A typical page: most sections use the default fade-up, while the hero and banner each have their own choreography.
<section class="hero" data-scroll data-scroll-preset="hero">
<picture class="hero__picture">…</picture>
<div class="hero__box">
<h1 class="hero__title">Welcome</h1>
<p class="hero__text">Lead paragraph</p>
<a class="hero__button" href="/start">Get started</a>
</div>
</section>
<!-- Default fade-up: just add data-scroll + data-scroll-item -->
<section data-scroll>
<h2 data-scroll-item>About us</h2>
<p data-scroll-item>One paragraph</p>
<p data-scroll-item>Another paragraph</p>
</section>
<section class="banner" data-scroll data-scroll-preset="banner">
<picture class="banner__picture">…</picture>
<p class="banner__subtitle">Featured</p>
<h2 class="banner__title">A bold claim</h2>
<p class="banner__text">Supporting copy</p>
<a class="banner__button" href="/more">Read more</a>
</section>import { initScrollTrigger, registerPreset } from '@vettvangur/scroll-trigger'
registerPreset('hero', ({ section, gsap }) => {
const tl = gsap.timeline({ paused: true })
const config = { duration: 1.5, ease: 'power3.out' }
tl.from(section.querySelector('.hero__picture'), { opacity: 0, scale: 1.1, ...config }, 0)
.from(section.querySelector('.hero__box'), { opacity: 0, y: 30, ...config }, 0)
.from(section.querySelector('.hero__title'), { opacity: 0, ...config }, 0.1)
.from(section.querySelector('.hero__text'), { opacity: 0, ...config }, 0.3)
.from(section.querySelector('.hero__button'), { opacity: 0, ...config }, 0.5)
return tl
})
registerPreset('banner', ({ section, gsap }) => {
const tl = gsap.timeline({ paused: true })
const config = { duration: 1.5, ease: 'power3.out' }
tl.from(section.querySelector('.banner__picture'), { scale: 1.1, ...config }, 0)
.from(section.querySelector('.banner__subtitle'), { opacity: 0, ...config }, 0)
.from(section.querySelector('.banner__title'), { opacity: 0, ...config }, 0.2)
.from(section.querySelector('.banner__text'), { opacity: 0, ...config }, 0.3)
.from(section.querySelector('.banner__button'), { opacity: 0, ...config }, 0.4)
return tl
})
initScrollTrigger()Section-by-section behavior:
- Hero — picks up
data-scroll-preset="hero"and runs the timeline above. - About — no preset attribute, so it falls through to the default
fade-upand animates each[data-scroll-item]child with a 0.15s stagger. - Banner — picks up
data-scroll-preset="banner".
Register the presets before calling initScrollTrigger. After init, the returned cleanup function tears down every trigger created by that call.
Options
initScrollTrigger({
root: document, // or any ParentNode
selector: '[data-scroll]',
itemSelector: '[data-scroll-item]',
start: 'top 80%', // ScrollTrigger start
end: 'top bottom', // ScrollTrigger end
markers: 'auto', // 'auto' | true | false
reducedMotion: 'respect', // 'respect' | 'ignore'
defaultPreset: 'fade-up',
})markers: 'auto'— markers show onlocalhost,127.0.0.1, and*.localhosts; off everywhere else. Passtrue/falseto override.reducedMotion: 'respect'— when the user prefers reduced motion, animations are skipped and content renders in its natural state. No ScrollTriggers are created.- The returned cleanup function kills only the triggers this call created.
Built-in presets
| Preset | Effect |
|--------|--------|
| fade-up | Fade in + translate from y: 30 |
| fade-in | Fade in only |
| scale-in | Fade in + scale from 0.92 |
When a section has 2+ [data-scroll-item] children, items animate with a 0.15s stagger.
Custom presets
Register a builder once before calling initScrollTrigger. The builder receives { section, items, gsap, reducedMotion } and returns a paused timeline.
import { initScrollTrigger, registerPreset } from '@vettvangur/scroll-trigger'
registerPreset('hero', ({ section, gsap }) => {
const tl = gsap.timeline({ paused: true })
tl.from(section.querySelector('.hero__picture'), {
opacity: 0,
scale: 1.1,
duration: 1.5,
ease: 'power3.out',
}, 0)
.from(section.querySelector('.hero__title'), {
opacity: 0,
y: 30,
duration: 1.5,
ease: 'power3.out',
}, 0.1)
.from(section.querySelector('.hero__text'), {
opacity: 0,
y: 30,
duration: 1.5,
ease: 'power3.out',
}, 0.3)
return tl
})
initScrollTrigger()<section class="hero" data-scroll data-scroll-preset="hero">
<picture class="hero__picture">…</picture>
<h1 class="hero__title">…</h1>
<p class="hero__text">…</p>
</section>Cleanup
initScrollTrigger returns a function that kills the triggers it created — call it on SPA route changes or component unmount:
const cleanup = initScrollTrigger()
// later…
cleanup()For a nuclear reset (kills every ScrollTrigger on the page, including ones created elsewhere):
import { killScrollTriggers } from '@vettvangur/scroll-trigger'
killScrollTriggers()Refreshing
Layout-affecting changes (lazy images, accordions opening, dynamic content) may invalidate trigger positions. Webfonts are handled automatically; for everything else:
import { refreshScrollTriggers } from '@vettvangur/scroll-trigger'
refreshScrollTriggers()Window resize is handled by GSAP itself (debounced 200ms).
React
import { initScrollTrigger } from '@vettvangur/scroll-trigger'
import { useEffect } from 'react'
export default function Layout({ children }) {
useEffect(() => initScrollTrigger(), [])
return <>{children}</>
}The cleanup returned by initScrollTrigger is the effect's teardown — triggers are killed on unmount.
Behavior notes
- The plugin is registered once on the first call to
initScrollTrigger. - Triggers use
toggleActions: 'play none none none'— the animation plays on enter and stays in its end state. Scrolling back up does not reverse it. gsap.from()immediate-renders the from-state, so items are hidden as soon asinitScrollTriggerruns (avoids a flash of un-animated content).- An
'auto'marker host can be forced on by appending.localto a hostname in/etc/hosts— useful when you want markers in a staging domain.
