npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vettvangur/scroll-trigger

v0.0.6

Published

Vettvangur | Declarative GSAP ScrollTrigger helper

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 gsap

gsap 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-up and 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 on localhost, 127.0.0.1, and *.local hosts; off everywhere else. Pass true / false to 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 as initScrollTrigger runs (avoids a flash of un-animated content).
  • An 'auto' marker host can be forced on by appending .local to a hostname in /etc/hosts — useful when you want markers in a staging domain.