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

saketjs

v0.6.1

Published

A lightweight vanilla JS library for mouse followers, magnetic buttons, image hover effects, text animations (3D reveal, bounce, scramble) and decorative text styles (neon, glitch, matrix, melt, and more), smooth scroll, parallax, marquees, ripples, a fle

Readme

SaketJS

npm version npm downloads license bundle size

A lightweight vanilla JS effects library — 20 effects covering cursors, magnets, image hovers, text animations, smooth scroll, parallax, marquees, ripples, loaders (preloader, counter, SVG logo, spinner, image skeleton), a flexible string divider, a particle network background, clip-path reveals, image sequences, a hamburger menu overlay, a raw-WebGL/GLSL liquid image distortion, and multi-style page transitions (with an optional Barba-style AJAX mode) — built small and dependency-light on purpose.

No framework required. No separate GSAP install required — it's bundled inside. One command in your terminal, one import, you're animating.

npm install saketjs

Quick Start

import Saket from 'saketjs';
import 'saketjs/dist/saketjs.css';

Saket.mouseFollower('body', { type: 'ring' });
Saket.magnet('.magnet-btn');
Saket.textEffect('h1', { type: 'stagger' });
Saket.pageTransition({ type: 'fade' });

That's the entire setup — GSAP is bundled inside the package itself, so npm install saketjs is the only install command you ever need to run. No npm install gsap separately, no peer dependency warnings.

Contents

Installation

Run this in your project's terminal:

npm install saketjs

This adds saketjs (and its one bundled dependency, gsap) to your package.json and downloads it into node_modules. You're ready to import it right away — see Usage below.

Usage

With a bundler (Webpack / Vite / Parcel / Next.js etc.) — recommended after npm install

import Saket from 'saketjs';
import 'saketjs/dist/saketjs.css';

Saket.mouseFollower('body', { type: 'ring' });

Without a bundler (plain HTML page)

Copy node_modules/saketjs/dist/saketjs.umd.js and saketjs.css next to your HTML, or reference them directly:

<link rel="stylesheet" href="node_modules/saketjs/dist/saketjs.css" />

<script src="node_modules/saketjs/dist/saketjs.umd.js"></script>
<script>
  Saket.mouseFollower('body', { type: 'ring' });
</script>

That's it — GSAP is already bundled inside saketjs.umd.js, so there is nothing else to include.


API Reference

1. Saket.mouseFollower(container, options)

A custom cursor that trails the real mouse pointer.

| Option | Default | Description | |---|---|---| | type | 'dot' | 'dot' | 'ring' | 'blob' | | size | 20 | Diameter in px | | color | '#ffffff' | Follower color | | ease | 0.15 | Trailing smoothness (lower = smoother/laggier) | | hoverSelector | 'a, button, [data-saket-hover]' | Elements that scale the follower up on hover | | hoverScale | 2.5 | Scale factor while hovering a target | | mixBlendMode | 'difference' | CSS blend mode applied to the follower |

Saket.mouseFollower('body', { type: 'blob', color: '#8a7fff', size: 30 });

Returns a destroy() function you can call to remove the follower and its listeners.


2. Saket.magnet(selector, options)

Pulls matched elements toward the cursor when it's nearby, and springs them back on mouseleave.

| Option | Default | Description | |---|---|---| | strength | 0.5 | 0–1, how strongly the element follows the cursor | | radius | 80 | Extra px beyond the element's own box that still triggers the pull | | ease | 0.3 | Seconds, snap-back speed | | scaleOnHover | 1 | Set e.g. 1.05 to also grow slightly while pulled |

Saket.magnet('.magnet-btn', { strength: 0.6, radius: 100 });

3. Saket.imageHover(selector, options)

selector should match a container wrapping an <img> (or a background-image div).

| Option | Default | Description | |---|---|---| | type | 'tilt' | 'tilt' | 'reveal' | | intensity | 15 | Max tilt rotation in degrees ('tilt' only) | | scale | 1.08 | Zoom amount on hover | | overlayColor | 'rgba(0,0,0,0.35)' | Overlay color swept across the image ('reveal' only) |

Saket.imageHover('.project-thumb', { type: 'reveal', overlayColor: 'rgba(138,127,255,0.5)' });

4. Saket.textEffect(selector, options)

| Option | Default | Description | |---|---|---| | type | 'stagger' | 'stagger' (reveal on scroll) | 'scramble' (shuffle on hover) | 'reveal3d' (3D rotated reveal on scroll) | 'bounce' (elastic drop-in per unit) | | splitBy | 'char' | 'char' | 'word' ('stagger'/'reveal3d'/'bounce' only) | | bounceDistance | 40 | px each unit drops from ('bounce' only) | | stagger | 0.03 | Delay between each unit's animation | | duration | 0.6 | Animation duration per unit | | scrambleChars | A–Z | Character set used while scrambling | | scrambleSpeed | 30 | ms between character flickers ('scramble' only) | | rotateX / rotateY | -90 / 0 | Starting rotation in degrees ('reveal3d' only) | | perspective | 600 | px, 3D depth ('reveal3d' only) | | reverseOnLeave | true | Re-hides when scrolled back past ('reveal3d' only) |

Saket.textEffect('h1.headline', { type: 'stagger', splitBy: 'word' });
Saket.textEffect('.nav-link', { type: 'scramble' });
Saket.textEffect('h1.hero-title', { type: 'reveal3d', splitBy: 'word' });

5. Saket.pageTransition(options)

Two ways to use it:

mode: 'reload' (default) — works on any plain multi-page site, no router needed. Covers the overlay on link click, does a real navigation, then reveals it on the next page's load.

mode: 'ajax' — inspired by Barba.js: instead of a full page reload, fetches the destination page's HTML and swaps just one container's contents, so your header/nav never flickers or re-initializes. Requires the same element, with the same attribute, present on every page:

<!-- identical wrapper on every page of your site -->
<main data-saket-transition-container>
  ...page-specific content...
</main>
Saket.pageTransition({ mode: 'ajax', type: 'slideLeft' });

| Option | Default | Description | |---|---|---| | color | '#111111' | Overlay color | | duration | 0.6 | Animation duration in seconds | | linkSelector | 'a[href]' | Which links trigger the transition | | type | 'wipe' | 'wipe' | 'slideLeft' | 'slideRight' | 'slideUp' | 'slideDown' | 'fade' | 'scaleFade' | 'scribble' | | scribbleThickness | '250vmax' | How thick the scribble stroke grows to fully cover the screen ('scribble' only) | | mode | 'reload' | 'reload' | 'ajax' | | containerSelector | '[data-saket-transition-container]' | Element swapped in 'ajax' mode | | onNavigate | null | 'ajax' mode only — callback fired after new content is swapped in, so you can re-run any per-page effects (magnet, ripple, etc.) on the fresh DOM |

Saket.pageTransition({ color: '#0d0d0f', type: 'fade' });

Call this once per page (near the bottom of your <body>), on every page of your site, so the transition stays consistent across navigation.

To skip the transition on a specific link (e.g. a download link), add data-saket-no-transition to it.

Note: saketjs.css keeps the overlay visible by default so there's no flash of unstyled content before JS runs. This means the page assumes JavaScript is enabled; that's expected for an effects library like this. In 'ajax' mode, remember that any Saket effects targeting elements inside the swapped container need to be re-initialized after navigation — use the onNavigate callback for that.


6. Saket.smoothScroll(options)

A tiny dependency-free stand-in for Locomotive Scroll — fakes native scroll with a lerped transform for that heavier, cinematic feel.

<div data-saket-scroll>
  <!-- all your page content goes inside here -->
</div>
Saket.smoothScroll({ ease: 0.1 });

| Option | Default | Description | |---|---|---| | wrapperSelector | '[data-saket-scroll]' | The element wrapping all scrollable content | | ease | 0.1 | Lower = smoother/heavier trailing | | direction | 'vertical' | 'vertical' | 'horizontal' |


7. Saket.parallax(selector, options)

Moves matched elements at a different speed than the page scroll.

| Option | Default | Description | |---|---|---| | speed | 0.3 | -1..1 — negative reverses direction, 0 disables movement |

Saket.parallax('.hero-image', { speed: -0.2 });

8. Saket.marquee(selector, options)

Turns any element's content into a seamless, infinitely scrolling strip.

| Option | Default | Description | |---|---|---| | speed | 60 | px per second | | direction | 'left' | 'left' | 'right' | | pauseOnHover | true | Pause the scroll while the cursor is over it |

Saket.marquee('.logo-strip', { speed: 80, direction: 'right' });

9. Saket.ripple(selector, options)

Material-style expanding ripple centered on the click point.

| Option | Default | Description | |---|---|---| | color | 'rgba(255,255,255,0.5)' | Ripple color | | duration | 0.6 | Seconds for the ripple to expand and fade |

Saket.ripple('button', { color: 'rgba(138,127,255,0.6)' });

10. Saket.preloader(options)

Full-screen loader shown immediately on page load, fading out once the page (and all assets) finish loading.

| Option | Default | Description | |---|---|---| | background | '#0d0d0f' | Loader background color | | text | '' | Optional loading text/logo | | minDuration | 0.6 | Minimum seconds the loader stays visible, even if the page loads instantly | | onComplete | null | Callback fired right after the loader is removed |

Saket.preloader({ text: 'LOADING', minDuration: 1 });

11. Saket.string(selector, options)

Turns any empty container into a flexible "string" divider — a line that bows toward the cursor's vertical position as it moves across the container, and springs back to a flat rest position on mouseleave. Pure SVG + GSAP, no Three.js/WebGL involved, so it stays lightweight.

<div class="my-divider"></div>
Saket.string('.my-divider', {
  color: '#8a7fff',
  thickness: 2,
  height: 160,
  strength: 1.2
});

| Option | Default | Description | |---|---|---| | color | '#000000' | Line color | | thickness | 2 | Stroke width in px | | height | 160 | Total vertical space (in px) the string is allowed to bow into | | restY | null | Resting Y position of the string; defaults to height / 2 (centered) | | strength | 1 | 0–2+, how far the string bows toward the cursor — higher is more elastic/exaggerated | | padding | 10 | Horizontal inset so the string doesn't touch the container's edges | | enterEase | 'power4.out' | Easing while the string follows the cursor | | enterDuration | 0.4 | Seconds to catch up to the cursor | | leaveEase | 'elastic.out(1, 0.4)' | Easing for the spring-back on mouseleave | | leaveDuration | 0.7 | Seconds for the spring-back animation |

You don't need to write any SVG yourself — SaketJS injects it into the container automatically, sized to the container's own width, and re-measures on window resize.


12. Saket.svgLoader(options)

A full-screen loader built around your own logo mark: it draws your SVG's strokes in, then a set of panels wipe away to reveal the page.

Saket.svgLoader({
  svg: '<svg viewBox="0 0 134 229">...your logo markup...</svg>',
  strokeColor: '#ffffff',
  panelCount: 5,
  panelColor: '#0d0d0f',
  onComplete: () => console.log('loader done')
});

| Option | Default | Description | |---|---|---| | svg | '' | Raw <svg>...</svg> markup for your logo/mark — required | | strokeColor | '#ffffff' | Color applied to the logo's strokes as they draw in | | panelCount | 5 | Number of vertical panels used for the reveal wipe | | panelColor | '#0d0d0f' | Panel color | | background | '#0d0d0f' | Loader background, behind the logo | | drawDuration | 1.2 | Seconds for the logo's strokes to draw in | | holdDuration | 0.4 | Pause after drawing, before the panels wipe | | panelDuration | 0.9 | Seconds for the panel wipe | | onComplete | null | Callback fired after the loader is fully removed |


13. Saket.counterLoader(options)

A full-screen loader that counts up to 100% at a slightly randomized pace, then a row of bars shrinks away to reveal the page.

Saket.counterLoader({
  background: '#0d0d0f',
  textColor: '#ffffff',
  barCount: 8
});

| Option | Default | Description | |---|---|---| | background | '#0d0d0f' | Loader background | | textColor | '#ffffff' | Counter text color | | fontSize | '5rem' | Counter text size | | barCount | 8 | Number of reveal bars | | barColor | '#111111' | Bar color | | minStep / maxStep | 1 / 10 | Range for each random count increment | | tickDelayMin / tickDelayMax | 40 / 160 | ms range between count updates | | barDuration | 1.2 | Seconds for the bar reveal | | onComplete | null | Callback fired after the loader is fully removed |


14. Saket.particleNetwork(selector, options)

A lightweight canvas "connected dots" background — no external particle library, just <canvas> + requestAnimationFrame. Particles drift and link to nearby neighbors with a line; optionally get pushed away from the cursor.

<div class="hero-bg" style="position: relative; height: 100vh;"></div>
Saket.particleNetwork('.hero-bg', {
  particleCount: 80,
  color: '#8a7fff',
  interactive: true
});

| Option | Default | Description | |---|---|---| | particleCount | 80 | Number of particles | | color | '#8a7fff' | Particle fill color | | linkColor | '#8a7fff' | Line color between nearby particles | | linkDistance | 140 | px — particles closer than this get linked | | linkOpacity | 0.25 | Max opacity of link lines | | particleSize | 2 | Particle radius in px | | speed | 0.4 | Drift speed | | interactive | true | Whether particles get pushed away from the cursor | | repulseDistance | 100 | px radius of the cursor's push effect | | repulseStrength | 1.5 | How hard particles get pushed |

The container needs position: relative (or similar) and an explicit height — the canvas fills it completely via absolute positioning.


15. Saket.clipTitle(selector, options)

Reveals a title or block by animating its clip-path open — starts as a sliver and expands to show the full element.

Saket.clipTitle('.big-title', { shape: 'diamond', duration: 1 });

| Option | Default | Description | |---|---|---| | shape | 'diamond' | 'diamond' | 'rectLeft' | 'rectUp' | 'circle' | | duration | 1 | Seconds for the reveal | | ease | 'power3.inOut' | GSAP easing | | trigger | 'view' | 'view' (reveals on scroll into view) | 'immediate' |


16. Saket.imageSequence(selector, options)

Cycles an <img>'s src through a list of images — a hero image that flickers through a sequence, or a hover-scrub product preview.

Saket.imageSequence('.hero-img', {
  images: Array.from({ length: 10 }, (_, i) => `/images/frame-${i + 1}.jpg`),
  interval: 250,
  trigger: 'auto'
});

| Option | Default | Description | |---|---|---| | images | [] | Array of image URLs to cycle through — required | | interval | 250 | ms between frames | | trigger | 'auto' | 'auto' (always cycling) | 'hover' (cycles only while hovered) | | loop | true | Whether it loops back to the first image after the last |


17. Saket.spinner(selector, options)

A pure-CSS loading spinner injected into each matched container. 6 variants.

Saket.spinner('.loading-slot', { type: 'ring', size: 40, color: '#8a7fff' });

| Option | Default | Description | |---|---|---| | type | 'dots' | 'dots' | 'bars' | 'ring' | 'plane' | 'grid' | 'wander' | | size | 40 | px | | color | '#8a7fff' | Spinner color |


18. Saket.imageLoader(selector, options)

Wraps an <img> with a pulsing skeleton placeholder shown until the image finishes loading, then fades the real image in. Handles already-cached images too.

Saket.imageLoader('.product-photo', { minDuration: 300 });

| Option | Default | Description | |---|---|---| | minDuration | 300 | ms — keeps the skeleton visible at least this long, so cached/instant images don't just flash |


19. Saket.menuToggle(triggerSelector, menuSelector, options)

Wires a hamburger-icon trigger (SaketJS injects and morphs the 3 bars into an X) with a full-screen overlay menu whose links stagger into view.

<button class="my-trigger"></button>
<nav class="my-menu"><a href="#">Home</a><a href="#">About</a></nav>
Saket.menuToggle('.my-trigger', '.my-menu', { color: '#0d0d0f' });

| Option | Default | Description | |---|---|---| | color | '#0d0d0f' | Overlay background | | linkSelector | 'a' | Links inside the menu that get the stagger reveal | | stagger | 0.06 | Delay between each link's entrance | | duration | 0.6 | Animation duration | | closeOnLinkClick | true | Auto-close when a menu link is clicked | | closeOnEscape | true | Auto-close on the Escape key |


20. Saket.liquidImage(selector, options)

A raw WebGL + hand-written GLSL hover-distortion effect — the image ripples away from the cursor, with ripple strength driven by cursor speed. No Three.js, no dependency at all; this is genuinely custom shader code.

Saket.liquidImage('.hero-photo', { intensity: 0.06, radius: 0.25 });

| Option | Default | Description | |---|---|---| | intensity | 0.06 | Max UV displacement — how strong the ripple distortion gets | | radius | 0.25 | 0–1, how far the ripple reaches from the cursor | | decay | 0.92 | How fast the ripple settles after the cursor stops (closer to 1 = lingers longer) | | velocityBoost | 18 | How strongly cursor speed pumps up the ripple strength |

Important: if the image is loaded from a different origin, that server must send CORS headers (Access-Control-Allow-Origin) or the browser will refuse to read its pixels into WebGL — same-origin images always work without any extra setup. If WebGL isn't available in the browser, this effect silently skips that element rather than breaking the page.


21. Saket.textStyle(selector, options)

A collection of decorative text treatments — distinct from textEffect, which is about scroll/hover behavior. Most of these are CSS-driven visual styles rather than JS-computed reveals.

Saket.textStyle('.hero-title', { style: 'neon', color: '#ff005e', colorAlt: '#00d4ff' });

| Option | Default | Description | |---|---|---| | style | 'neon' | see the table below | | color | '#ff005e' | Primary color (meaning varies slightly per style) | | colorAlt | '#00d4ff' | Secondary color | | radius | 110 | px, used by 'cylinder3d' | | words | [] | used by 'rollCarousel' — defaults to the element's own text as a single word | | wordDuration | 1.5 | seconds each word holds, used by 'rollCarousel' | | leftText / rightText | null | used by 'split' — defaults to auto-splitting the element's text in half |

| style value | What it does | |---|---| | 'neon' | Alternating-color glowing text shadow | | 'glitch' | Two clipped, offset duplicate layers jitter for a digital-glitch look | | 'matrix' | Green glowing text over a falling "code rain" background, with a glitch-clipped duplicate layer | | 'melt' | Gradient-filled text with a blurred, dripping duplicate layer beneath it | | 'shadowDance' | Two hard-edged colored shadows swap diagonal position back and forth | | 'explode' | On hover, two radial-gradient rings burst outward from the text | | 'wave' | Splits into characters; each bobs up and down with a staggered delay | | 'split' | Splits the text in half; each half slides in from opposite screen edges | | 'marquee3d' | Two angled, perspective-rotated panels each scroll a copy of the text across them | | 'nabla' | Uses the variable Nabla font's depth axis to pulse each character's 3D extrusion (loads the font automatically) | | 'cylinder3d' | Splits into characters and arranges them around a rotating 3D cylinder | | 'rollCarousel' | Cycles through a list of words, each rolling in, holding, then rolling away |

Saket.textStyle('.glitchy', { style: 'glitch', color: '#ff005e', colorAlt: '#00d4ff' });
Saket.textStyle('.spinning-words', { style: 'rollCarousel', words: ['fast', 'simple', 'yours'] });

Note on 'nabla': it loads the Nabla variable font from Google Fonts on first use — that's a network request, so it only applies if the page has internet access.


Project structure (for local development)

saketjs/
├── src/                  # source, edit here
│   ├── index.js
│   ├── effects/
│   └── utils/
├── styles/saketjs.css
├── dist/                 # generated — do not edit by hand
├── demo/                 # live demo pages
└── build.js              # esbuild bundling script

Building from source

npm install
npm run build     # outputs dist/saketjs.esm.js, .cjs.js, .umd.js, .umd.min.js, .css

Then open demo/index.html directly in a browser to see every effect running.

Links

License

MIT