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

classy-scroll

v2.0.0

Published

A lightweight, performance-first library for scroll-based class toggling. Built with the IntersectionObserver API and TypeScript.

Readme

Classy Scroll 🎩✨

A lightweight, performance-first library for scroll-based class toggling. Built with the IntersectionObserver API and TypeScript.

💡 The Philosophy

We believe in Separation of Concerns.

  • JavaScript handles the trigger (when to start).
  • CSS handles the animation (what happens).

This library does not include physics engines, timelines, or parallax math. It does one thing efficiently: it toggles a class when an element enters the viewport.

By default, classes are persistent and stay forever once added (perfect for reveal animations). Set persistent: false for bidirectional toggling (useful for progress indicators, nav highlights, etc.).

When to use Classy Scroll?

| If you need... | Use... | | :---- | :---- | | Complex Timelines (Pinning, Scrubbing, Parallax) | GSAP ScrollTrigger (The industry standard) | | React-Specific Physics (Springs, Gestures) | Framer Motion | | Pre-made Animations (Standard fades/slides) | AOS (Great if you skip writing CSS) | | Total Creative Freedom (Any CSS class toggle) | Classy Scroll 🎩 |

Classy Scroll is designed for the 90% use case: You want to trigger CSS transitions as you scroll, and you want it to be fast, accessible, and lightweight.

Ghost Element Architecture: v2.0 introduces "Ghost Elements." The library creates invisible clones of your targets to track intersections. This ensures that your CSS transforms (like translateY(100px)) never interfere with the scroll trigger math.

Future-Proof Architecture: By strictly delegating animation logic to CSS, this library is inherently forward-compatible. As browsers adopt new CSS features (like discrete property transitions), your animations gain those powers immediately without needing library updates.

📦 Installation

npm install classy-scroll  
# or  
pnpm add classy-scroll

🛠 Usage & Recipes

1. The "Minimal" Way (Vanilla CSS)

By default, the library toggles the class is-visible. You define your start state in CSS, and your end state using the toggled class.

<div class="animated-element">Hello World</div>
 
<style>  
  /* Start State (Hidden) */  
  .animated-element {  
    opacity: 0;  
    transform: translateY(20px);  
    transition: opacity 0.6s, transform 0.6s;  
  }

  /* End State (Added by library) */  
  .animated-element.is-visible {  
    opacity: 1;  
    transform: translateY(0);  
  }  
</style>

<script>  
  import { classyScroll } from 'classy-scroll';  
    
  // Just pass the selector!  
  classyScroll('.animated-element');  
</script>

2. With Tailwind CSS 🌊

Classy Scroll works perfectly with utility classes. You set the "Start" utility classes in your HTML, and pass the "End" utility classes to the library.

<div class="opacity-0 translate-y-8 transition-all duration-700 animated-element">  
  I fade in with Tailwind!  
</div>

<script>  
  import { classyScroll } from 'classy-scroll';

  classyScroll('.animated-element', {  
    // Pass the Tailwind utilities to add when visible  
    class: 'opacity-100 translate-y-0',   
    threshold: 0.5  
  });  
</script>

3. Full Configuration

For TypeScript users, here is the complete interface definition showing all available options and their types.

export interface ClassyScrollOptions {
  /** Space-separated classes to add when element is in view. Default: 'is-visible' */
  class?: string;
  /** Fraction of the element (0.0–1.0) that must be visible to trigger. Default: 0.1 */
  threshold?: number;
  /** Margin around the root element (e.g. "10px 0px"). Default: '0px' */
  rootMargin?: string;
  /** If true, the class stays after being added. If false, the class toggles on/off. Default: true */
  persistent?: boolean;
  /** Delay in ms between elements in the same batch. Default: 0 */
  stagger?: number;
  /** Global delay in ms before animation starts. Default: 0 */
  delay?: number;
  /** Enable debug overlay to visualize trigger zones. Default: false */
  debug?: boolean;
  /** Callback fired when element intersects. */
  callback?: (element: HTMLElement) => void;
}

Example Instantiation:

import { classyScroll } from 'classy-scroll';

classyScroll('.animated-element', {  
  class: 'my-active-class',  
  threshold: 0.5,   
  rootMargin: '-50px 0px',   
  persistent: true,  
  stagger: 100,  
  debug: true,  
  callback: (element) => console.log('Animated:', element)  
});

⚙️ API

classyScroll(targets, options)

Arguments:

  • targets: string (selector), NodeList, HTMLElement[], or single HTMLElement.
    • Note: Unlike v1.x, the library no longer uses MutationObserver to watch for new elements. For dynamic content, re-initialize the library or use framework lifecycle hooks.
  • options: Configuration object (optional).

HTML Data Attributes

You can override global settings on a per-element basis.

<div
  class="box"   
  data-cs-delay="500"   
  data-cs-class="special-pop"
>  
  I wait 500ms and get a custom class  
</div>

🕵️ Debug Mode

Debugging scroll interactions is usually painful. Enable debug: true to visualize exactly where your trigger zones are.

classyScroll('.box', {  
  debug: true,  
  rootMargin: '-20% 0px', // Trigger line moves 20% up from bottom
  threshold: 0.5
});

What You'll See:

Our high-performance Canvas 2D overlay paints your scroll math in real-time without cluttering the DOM:

  • Dashed Lines (Viewport Margins):
    • Red Dashed Line: Your Top rootMargin boundary (Exit zone).
    • Green Dashed Line: Your Bottom rootMargin boundary (Entry zone).
  • Solid Tracking Lines (Element Thresholds):
    • Blue Line (En): Tracks the exact pixel where the element Enters (based on your threshold %).
    • Blue Line (Ex): Tracks the exact pixel where the element Exits (only active when persistent: false).
  • Dynamic Labels: Right-aligned badges (e.g., [.js-fade] Entry: -10% 0px) linking the specific threshold to its target element.

How to use it: Simply scroll and watch the solid Element lines (Blue) collide with the dashed Viewport lines (Red/Green). The moment they touch, your CSS classes swap!

🧩 Framework Examples

React / Next.js (Dynamic Content)

Since v2.0, the library no longer uses a global MutationObserver. For dynamic lists, initialize the library inside a useEffect and include your data in the dependency array. This ensures new items are registered and old "ghost" elements are cleaned up correctly.

import { useState, useEffect } from 'react';  
import { classyScroll } from 'classy-scroll';

export const DynamicList = () => {  
  const [items, setItems] = useState([1, 2, 3]);

  useEffect(() => {  
    // Re-initialize when items change to account for new DOM nodes
    const { destroy } = classyScroll('.animated-element', {   
        stagger: 100,  
        persistent: true   
    });  
    return () => destroy();  
  }, [items]); // The "items" dependency replaces the old MutationObserver logic

  return (  
    <div>  
      <button onClick={() => setItems([...items, items.length + 1])}>  
        Add Item  
      </button>  
        
      {items.map(i => (  
        // This new item will animate automatically when added!  
        <div key={i} className="animated-element">  
          Item {i}  
        </div>  
      ))}  
    </div>  
  );  
};

Vue / Nuxt

Works great with onMounted.

<script setup>  
import { onMounted, onUnmounted } from 'vue';  
import { classyScroll } from 'classy-scroll';

let scroller;

onMounted(() => {  
  scroller = classyScroll('.animated-element', { stagger: 50 });  
});

onUnmounted(() => {  
  scroller?.destroy();  
});  
</script>

🚀 Migration Guide (v1.x → v2.0)

Breaking Changes

  1. MutationObserver Removed: The library no longer automatically watches the DOM. You must call classyScroll when new elements are added or use framework lifecycle hooks (like useEffect or onMounted).
  2. once renamed to persistent: The default is now true.
  3. Threshold Narrowing: The threshold option now strictly accepts a single number (0.0 to 1.0). Arrays are no longer supported to ensure Ghost Element stability.

v1.x (Old):

classyScroll('.element', { once: true });  // Class added once
classyScroll('.element', { once: false }); // Class toggles
classyScroll('.element', { threshold: [0, 0.5, 1] }); // Array allowed

v2.x (New):

classyScroll('.element', { persistent: true });  // Class persists (default)
classyScroll('.element', { persistent: false }); // Class toggles
classyScroll('.element', { threshold: 0.5 }); // Must be a single number

License

MIT © 2026