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

atomicscroll

v1.0.0

Published

Smooth visible-scroll triggers and progressive scroll-driven animations in zero-dependency vanilla JavaScript and TypeScript.

Readme

atomicscroll

npm version License: MIT Zero Dependencies TypeScript

Smooth visible-scroll triggers and progressive scroll-driven animations in zero-dependency vanilla JavaScript and TypeScript.


✨ Features

  • 🚀 Zero Dependencies — Extremely lightweight (< 15 KB minified), no external runtime dependencies.
  • ⚡ Dual Mode Engine:
    • Trigger Mode: Uses IntersectionObserver to trigger style transitions when elements scroll into view.
    • Progressive Mode: Coalesces passive scroll events via requestAnimationFrame to interpolate 2D/3D transforms and opacity proportionally to scroll depth.
  • 📐 Pure Anchored Math — Normalized 0 to 1 progress formula anchored cleanly at element entry and exit points.
  • 🎨 2D & 3D Transform Interpolation — Full support for translate, scale, rotate, skew, decimal floating point values, and CSS units (px, deg, %, rem, etc.).
  • 🔤 CSS Custom Properties — Any --* variable works as a style key, and var() references in values are resolved for interpolated properties (transform, opacity, background-position, background-size).
  • ♿ Built-in Accessibility — Automatically respects prefers-reduced-motion: reduce by snapping to target styles without animation.
  • 📦 Universal Bundles — Ships ESM, CommonJS, and IIFE formats with TypeScript .d.ts declaration maps.
  • 🔌 Framework Agnostic — Native Vanilla JS core with first-class hooks for React, Vue, Svelte, Angular, and more.

⚡ Performance & Device Budgets

Both engines are built for smoothness, but they scale differently. Use this guide to size your animations for the weakest device you support.

How Each Engine Performs

| Engine | Mechanism | Cost Profile | |---|---|---| | Trigger (type: 'trigger') | IntersectionObserver viewport detection (off-main-thread) | Near-zero per-frame cost; style writes happen only on enter/leave. Scales to hundreds of elements. | | Progressive (type: 'progressive') | JS interpolation on scroll frames (passive listeners + requestAnimationFrame coalescing) | Cost scales with how many elements animate simultaneously. Parked offscreen elements cost only cheap layout reads because unchanged styles are never rewritten. |

Device Budgets (Field Estimates)

| Scenario | Guidance | |---|---| | Progressive elements animating simultaneously | Keep to ≤ 3–5 for steady 60fps on budget Android hardware; modern devices handle far more. | | Progressive elements registered but offscreen | Cheap (reads only) — ~30–50 is fine on low-end devices. | | Trigger-mode elements | Effectively unlimited. |

[!NOTE] These figures are estimates based on typical low-end hardware timings and vary with DOM/CSS complexity and other page activity. Validate element-heavy sections on real pages using DevTools CPU throttling before shipping.

Best Practices

  • Prefer trigger mode unless you need true scroll-linked interpolation.
  • Animate only transform and opacity — they are compositor-friendly and what the engine interpolates smoothly; other properties snap at progress boundaries.
  • Spread animated sections down the page instead of clustering many progressive elements inside a single viewport.
  • For heavily animated hero sections, add will-change: transform in your own CSS to hint layer promotion.
  • Call .destroy() when removing elements in SPA environments to detach listeners.
  • Reduced-motion users are handled automatically (respectReducedMotion), which also skips all per-frame work.

Known Scaling Limits

Progressive mode currently registers one passive scroll listener per instance and re-parses transform strings each frame. This is imperceptible at typical usage but is a documented optimization candidate if you need dozens of simultaneously animating elements on entry-level hardware.


🌐 Browser Support

| Browser | Minimum Version | |---|---| | Chrome / Edge | 80+ | | Firefox | 74+ | | Safari (macOS) | 13.1+ | | Safari (iOS / iPadOS) | 13.1+ |

  • Bundles are compiled with an ES2020 build target; browsers that cannot parse modern JS syntax (e.g., IE11) are not supported.
  • Trigger mode automatically falls back to passive scroll/resize visibility checks when IntersectionObserver is unavailable.
  • Honors the prefers-reduced-motion: reduce media query where supported.
  • Need older browsers (e.g., Safari 12, Chrome 60–79)? Lower the esbuild target (e.g., 'es2017') in build.mjs and rebuild — no source changes required.

📦 Installation

# npm
npm install atomicscroll

# yarn
yarn add atomicscroll

# pnpm
pnpm add atomicscroll

Browser CDN / Direct Script Tag

<script src="https://unpkg.com/atomicscroll/dist/atomicscroll.min.js"></script>
<script>
  // Global `AtomicScroll` is automatically available
  new AtomicScroll('.my-element', {
    type: 'trigger',
    newStyle: { transform: 'scale(1.1)', opacity: 1 }
  });
</script>

🚀 Quick Start

1. Trigger Mode (Intersection Trigger)

Triggers target styles once an element enters the viewport.

import { AtomicScroll } from 'atomicscroll';

const scroll = new AtomicScroll('#headline', {
  type: 'trigger',
  originalStyle: { opacity: 0, transform: 'translate(0px, 40px)' },
  newStyle: { opacity: 1, transform: 'translate(0px, 0px)' },
  once: true,
  threshold: 0.2,
  onEnter: (event) => console.log('Element entered viewport:', event.element),
});

2. Progressive Mode (Scroll-Driven Interpolation)

Interpolates transforms and opacity continuously as the element traverses the viewport.

import { AtomicScroll } from 'atomicscroll';

const scroll = new AtomicScroll('.hero-card', {
  type: 'progressive',
  originalStyle: { transform: 'scale(0.8) rotate(-5deg)', opacity: 0.4 },
  newStyle: { transform: 'scale(1.2) rotate(5deg)', opacity: 1 },
  onProgress: ({ progress, direction }) => {
    console.log(`Scroll progress: ${progress.toFixed(2)}, direction: ${direction}`);
  },
});

3. HTML Data-Attributes API

You can define styles directly in your HTML templates:

<!-- Trigger element -->
<div
  class="animate-on-scroll"
  data-at-type="trigger"
  data-at-to='{"transform":"translate(0px, 0px)","opacity":1}'
>
  Hello World
</div>

<!-- Progressive element -->
<div
  class="scroll-scale"
  data-at-type="progressive"
  data-at-from='{"transform":"scale(0.5)","opacity":0.2}'
  data-at-to='{"transform":"scale(1)","opacity":1}'
>
  Scaling content
</div>
// Auto-initializes from data-at-* attributes
new AtomicScroll('.animate-on-scroll');
new AtomicScroll('.scroll-scale');

🔤 CSS Variables & Custom Properties

Style maps accept any CSS custom property name — there is no --at- namespace restriction (the --at-* names in the demos are simply the demo design system's token convention).

const scroll = new AtomicScroll('.card', {
  type: 'trigger',
  originalStyle: { '--card-opacity': '0', opacity: 0 },
  newStyle: { '--card-opacity': '1', opacity: 1 },
});

var() references inside values are also supported:

<div
  class="hero"
  data-at-type="progressive"
  data-at-from='{"transform":"var(--tf-from)","opacity":"var(--opa-from)"}'
  data-at-to='{"transform":"var(--tf-to)","opacity":"var(--opa-to)"}'
>
  ...
</div>

Support depends on the mode and property:

| Property | Trigger mode | Progressive mode | |---|---|---| | transform, opacity, background-position, background-size | Applied verbatim; browser resolves var() natively | var() is pre-resolved via computed style, then interpolated numerically per frame | | All other properties (e.g. color, width) and custom-property keys (--*) | Applied verbatim | Snapped to the target value at progress >= 1, reverted below (values pass through verbatim, including var()) |

Notes:

  • Fallbacks are honored for interpolated properties: var(--missing, 0.4) falls back when the variable is undefined.
  • Arbitrary properties and custom-property keys are not numerically interpolated — they switch between the from/to values at full progress.

🧩 Framework Integration

React

import { useEffect, useMemo, useRef } from 'react';
import { AtomicScroll } from 'atomicscroll';

export function AnimatedBox() {
  const ref = useRef<HTMLDivElement>(null);

  // Stable identity matters: a fresh object literal per render would tear
  // down and rebuild the instance every render (visible under StrictMode).
  const options = useMemo(
    () => ({
      type: 'progressive' as const,
      originalStyle: { transform: 'translateY(50px)', opacity: 0 },
      newStyle: { transform: 'translateY(0px)', opacity: 1 },
    }),
    [],
  );

  useEffect(() => {
    if (!ref.current) return;
    const instance = new AtomicScroll(ref.current, options);
    return () => instance.destroy();
  }, [options]);

  return <div ref={ref} className="box">Animated Box</div>;
}

A complete runnable demo lives in examples/react/ (npm run demo:react).

Vue 3

<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { AtomicScroll } from 'atomicscroll';

const el = ref(null);
let instance = null;

onMounted(() => {
  if (el.value) {
    instance = new AtomicScroll(el.value, {
      type: 'trigger',
      newStyle: { opacity: 1, transform: 'scale(1)' },
    });
  }
});

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

<template>
  <div ref="el" class="box">Vue Animated Box</div>
</template>

Svelte

<script>
  import { onMount } from 'svelte';
  import { AtomicScroll } from 'atomicscroll';

  let el;

  onMount(() => {
    const instance = new AtomicScroll(el, {
      type: 'progressive',
      originalStyle: { transform: 'scale(0.5)' },
      newStyle: { transform: 'scale(1)' },
    });

    return () => instance.destroy();
  });
</script>

<div bind:this={el}>Svelte Animated Box</div>

⚙️ Options Reference (AtomicScrollOptions)

| Option | Type | Default | Description | |---|---|---|---| | type | 'trigger' \| 'progressive' | 'trigger' | Animation engine mode. | | originalStyle | StyleMap | undefined | Starting styles (falls back to computed CSS if omitted). | | newStyle | StyleMap | {} | Target styles to apply on trigger or interpolate toward. | | respectReducedMotion | boolean | true | Snaps immediately to target style when reduced motion is preferred. | | once | boolean | true | Trigger mode: apply and unobserve on first intersection (false re-arms). | | threshold | number \| number[] | 0 | IntersectionObserver threshold. | | rootMargin | string | '0px' | IntersectionObserver rootMargin. | | observerRoot | Element \| Document \| null | null | IntersectionObserver root element (defaults to viewport). | | scrollContainer | Element \| null | null | Progressive mode scroll driver (defaults to window). | | useDataAttributes | boolean | true | Reads data-at-* attributes from elements as configuration. | | onEnter | (e: ProgressEvent) => void | undefined | Callback fired when element enters viewport / begins progress. | | onLeave | (e: ProgressEvent) => void | undefined | Callback fired when element leaves viewport / ends progress. | | onProgress | (e: ProgressEvent) => void | undefined | Callback fired on progress change in progressive mode. |

Instance Methods

| Method | Description | |---|---| | instance.update(partialOptions) | Merge new options and re-initialize every element in place. | | instance.refresh() | Re-snapshot and re-initialize all elements without changing options. Call after external changes that alter resolved CSS custom properties — e.g. theme switches — so var()-driven from/to targets stay accurate. All demo pages wire their theme toggles to this. | | instance.init() | (Re-)initialize observers; after destroy() this rebuilds them, including re-arming the live reduced-motion listener. | | instance.destroy() | Tear down all listeners/observers and restore original inline styles. |

Live Reduced Motion

respectReducedMotion is evaluated at init and continuously: a matchMedia('(prefers-reduced-motion: reduce)') change listener re-evaluates every instance when the OS setting flips mid-session (elements snap to their final state, scroll work stops). Destroying an instance removes the listener.


📊 ProgressEvent Reference

Callbacks receive a ProgressEvent object:

interface ProgressEvent {
  element: HTMLElement;             // The animated element
  progress: number;                 // Normalized progress value (0 to 1)
  direction: 1 | -1 | 0;            // 1 = scrolling down, -1 = scrolling up, 0 = stationary
  type: 'trigger' | 'progressive';  // Observer mode
  entry?: IntersectionObserverEntry;// Available in trigger mode IO callbacks
}

🔄 Migration from Legacy API (0.x → 1.0.0)

| Legacy (v0.x) | Modern (v1.0.0) | |---|---| | AtScrollTriggerStyle('.selector') | new AtomicScroll('.selector', { type: 'trigger' }) | | AtScrollProgressiveStyle('.selector') | new AtomicScroll('.selector', { type: 'progressive' }) | | data-at-new-style='{...}' | data-at-to='{...}' | | data-at-original-style='{...}' | data-at-from='{...}' | | Implicit function split | data-at-type="trigger \| progressive" | | parseInt truncate scales (0.50) | Accurate parseFloat unit-aware interpolation |


🎨 Demo Styling & Assets (Atomic CSS)

The documentation and demo pages use atomic-css, Google Fonts (Plus Jakarta Sans & JetBrains Mono), and self-hosted local media assets (examples/assets/) for presentation, matching the AtomicScroll design system.

  • examples/atomic.min.css is a vendored standalone stylesheet (48,425 B) with a sidecar license at examples/LICENSE.atomic-css.
  • Media assets (examples/assets/) are self-hosted CC0 / Pexels licensed media managed via scripts/fetch-assets.mjs; see examples/assets/LICENSE.assets.md for full per-file attribution and checksums. Typography is loaded via Google Fonts CDN.
  • The core library itself (src/) has zero CSS, font, or asset dependencies and ships with sideEffects: false.

🛠️ Development

# Install dependencies
npm install

# Run TypeScript type check
npm run typecheck

# Run linter
npm run lint

# Run unit tests
npm test

# Run tests in watch mode
npm run test:watch

# Build production bundles
npm run build

# Start development server with live watch
npm run dev

# Start local static server
npm run serve

Deployment Notes

  • All demo pages reference assets with relative paths, so the repository root deploys as-is to static hosts (GitHub Pages project sites, Netlify, etc.) without path rewrites.
  • dist/ is committed because every demo page loads the runtime bundle from it at page load; rebuild with npm run build after changing src/.
  • examples/react/bundle.js is a prebuilt esbuild artifact (npm run demo:react) required by the React demo on static hosts.
  • scripts/serve.mjs sends X-Content-Type-Options: nosniff and a minimal CSP, and serves a styled 404.html for missing routes.

📄 License

MIT © 2026 Santosh Kunwar / codersantosh