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

@elementmints/snapscroll

v2.1.2

Published

Production-grade, accessibility-first fullpage scrolling engine. CSS Scroll Snap. Zero dependencies.

Readme

FullPage Engine

A production-grade, accessibility-first, fullpage scrolling library built on CSS Scroll Snap and native browser APIs. Zero dependencies. No scroll hijacking.

License: MIT


Why Another Fullpage Library?

Old approaches (fullPage.js, etc.) use JavaScript to manipulate scroll position via translateY or scrollTop. This breaks natural browser behavior, hurts accessibility, kills Lighthouse scores, and is incompatible with CSS Scroll Snap.

FullPage Engine is different:

| Feature | Old Approach | FullPage Engine | |---|---|---| | Scroll mechanism | JS position manipulation | CSS Scroll Snap (native) | | Animation | translateY entire page | scrollIntoView + CSS | | Passive listeners | ❌ Often blocking | ✅ Always passive | | prefers-reduced-motion | ❌ Rare | ✅ First class | | Content visibility | ❌ Hidden from crawlers | ✅ Always in DOM | | AEM author mode | ❌ Breaks authoring | ✅ Auto-detects, skips init | | CLS impact | ❌ High | ✅ Minimal (reserved dimensions) | | Dependencies | jQuery, extras | Zero |


Installation

# npm (when bundled)
npm install fullpage-engine

# Or copy the /js and /scss folders directly

Quick Start

HTML structure

<div id="fullpage">
  <section class="fp-section" data-fp-anchor="home">
    <h1>Section 1</h1>
  </section>
  <section class="fp-section" data-fp-anchor="about">
    <h2>Section 2</h2>
  </section>
</div>

JavaScript

import { FullPageEngine } from './js/init.js';

const fp = new FullPageEngine('#fullpage', {
  navigation: true,
  loop: false,
  progressBar: true,
});

Auto-init (no JS required)

<div id="fullpage" data-fp-auto data-fp-config='{"navigation":true}'>
  ...
</div>

Configuration

| Option | Type | Default | Description | |---|---|---|---| | navigation | boolean | true | Show dot navigation | | navigationPosition | 'left'|'right' | 'right' | Dot nav position | | navigationTooltips | string[] | [] | Labels for each section | | loop | boolean | false | Infinite vertical loop | | loopSlides | boolean | false | Infinite horizontal slides | | scrollingSpeed | number | 700 | Transition cooldown (ms) | | keyboardScrolling | boolean | true | Arrow key navigation | | lazyLoading | boolean | true | Lazy load media | | progressBar | boolean | false | Top progress bar | | recordHistory | boolean | true | Update browser URL | | animateAnchor | boolean | true | Animate on anchor nav | | responsiveWidth | number | 0 | Disable snap below px | | responsiveHeight | number | 0 | Disable snap below px | | autoplay | boolean | false | Autoplay slides | | autoplayInterval | number | 5000 | Autoplay interval (ms) | | gsap | Object\|null | null | GSAP instance for plugin | | plugins | FPPlugin[] | [] | Plugin array |


Callbacks

new FullPageEngine('#fp', {
  onInit(instance) { },

  // Return false to cancel navigation
  beforeLeave(originEl, destEl, direction) {
    return true;
  },

  onLeave(originEl, destEl, destIndex) { },
  afterLoad(sectionEl, index) { },
  onSlideLeave(sectionEl, originSlide, destSlide) { },
  afterSlideLoad(sectionEl, slideIndex) { },
  onResize({ width, height, isResponsive }) { },
});

Public API

const fp = new FullPageEngine('#fp');

fp.moveDown();              // Next section
fp.moveUp();                // Prev section
fp.moveTo(2);               // Go to index 2
fp.moveTo('about');         // Go to anchor
fp.moveToSlide(1);          // Go to slide in active section

fp.getActiveSection();      // → number
fp.getActiveSlide();        // → number

fp.on('fp:afterLoad', fn);  // DOM event listener
fp.use(myPlugin);           // Register plugin
fp.reinit();                // Re-initialize (AEM/SPA refresh)
fp.destroy();               // Full cleanup

Custom Events (DOM)

Listen on the wrapper element:

document.getElementById('fp').addEventListener('fp:afterLoad', (e) => {
  console.log(e.detail); // { section, index, anchor }
});

| Event | Detail | |---|---| | fp:init | { instance } | | fp:beforeLeave | { origin, destination, originIndex, destIndex } | | fp:onLeave | { origin, dest, destIdx } | | fp:afterLoad | { section, index, anchor } | | fp:slideLoad | { slideIndex, prevIndex, section } | | fp:resize | { width, height, isResponsive } | | fp:destroy | { instance } |


Horizontal Slides

Add .fp-slide children to any section:

<section class="fp-section" data-fp-anchor="work">
  <div class="fp-slides">
    <article class="fp-slide" data-fp-anchor="slide-1">Slide 1</article>
    <article class="fp-slide" data-fp-anchor="slide-2">Slide 2</article>
  </div>
</section>

Navigate with:

  • Arrow keys (← →)
  • Swipe gestures
  • Auto-rendered arrow buttons and slide dots

Overflow Sections

For sections with more content than 100vh:

<section class="fp-section" data-fp-overflow data-fp-anchor="blog">
  <div><!-- Lots of content --></div>
</section>

The section becomes internally scrollable. After scrolling to the bottom, the engine proceeds to the next section.


Lazy Loading

Mark media with data-fp-lazy-src:

<img data-fp-lazy-src="/hero.webp" width="1200" height="800" alt="Hero">

The engine uses IntersectionObserver with a 300px root margin to pre-load media as sections approach the viewport. Prevents render-blocking and reduces initial bundle size impact on LCP.


Entry Animations

Use data-fp-animate on any child element:

<section class="fp-section">
  <h1 data-fp-animate>Title</h1>
  <p data-fp-animate style="--fp-delay: 150ms">Staggered</p>
</section>

Animations use only opacity + transform — no layout triggers, no repaints. Automatically disabled with prefers-reduced-motion.


Plugin Architecture

const myPlugin = {
  name: 'analytics',

  onInit({ instance }) {
    console.log('FP ready');
  },

  onLoad({ section, index }) {
    // Track section views
    analytics.track('section_view', { id: section.id });
  },

  onLeave({ section }) { },
  onDestroy() { },
};

fp.use(myPlugin);

GSAP Plugin

import { createGSAPPlugin } from './js/init.js';

const fp = new FullPageEngine('#fp', {
  plugins: [createGSAPPlugin(gsap)],
});

Mark elements with data-gsap-from and data-gsap-to:

<h1 data-gsap-from='{"opacity":0,"y":40}' data-gsap-to='{"opacity":1,"y":0}'>
  Title
</h1>

Lenis Plugin

import { createLenisPlugin } from './js/init.js';

const lenis = new Lenis();
const fp = new FullPageEngine('#fp', {
  plugins: [createLenisPlugin(lenis)],
});

AEM Integration

FullPage Engine auto-detects AEM author mode using multiple strategies:

  • Granite namespace
  • CQ namespace
  • Body class flags (aem-AuthorLayer-Edit, wcm-mode-edit)
  • URL parameter ?wcmmode=edit
  • Presence of [data-cq-data-path] elements

When detected, initialization is skipped and .fp-author-mode is applied, which resets all snap behavior via CSS so authoring works normally.

Re-init after SPA/component refresh:

fp.reinit(); // or: fp.destroy(); fp = new FullPageEngine('#fp');

The MutationObserver inside the engine will auto-detect structural DOM changes and trigger a reinit.


Accessibility Checklist

  • ✅ Skip link injected before page
  • ✅ ARIA role="region" on all sections
  • aria-label from heading text or config
  • aria-hidden="true" on non-active sections
  • ✅ Live region announces section transitions
  • ✅ Full keyboard navigation (Arrow, PgUp, PgDn, Home, End)
  • ✅ Focus managed to new section on navigation
  • prefers-reduced-motion disables all animations
  • ✅ Navigation dots have proper aria-label and aria-current
  • ✅ Slide arrows have aria-label
  • ✅ Focus trapping respects overflow sections
  • ✅ All interactive elements have :focus-visible styles

Performance Architecture

LCP

  • Only the first section is rendered with content-visibility: visible
  • All others use content-visibility: auto (browser paints on demand)
  • Media is lazy-loaded via data-fp-lazy-src + IntersectionObserver
  • No render-blocking JavaScript (all type="module" or deferred)

CLS

  • Sections reserve their full height via min-height: 100svh
  • contain-intrinsic-size: auto 100svh preserves layout while content-visibility is auto
  • No DOM mutations during scroll animations
  • Image dimensions required via width/height attributes

INP

  • All event handlers use { passive: true }
  • No preventDefault on wheel/touch
  • Keyboard handler is lightweight (one keydown listener, O(1) lookup)
  • Scroll transitions use scrollIntoView — no JS animation loop

TBT

  • Modules are tree-shakable (ES module imports)
  • Observers are deferred
  • No synchronous blocking operations during init

FPS

  • CSS Scroll Snap handles all snapping — browser-native, GPU-accelerated
  • Only transform and opacity are animated (no layout, no paint)
  • will-change: contents applied only to active section
  • contain: layout paint on all sections
  • content-visibility: auto for off-screen paint elimination

Mobile Optimization

  • 100svh (small viewport height) prevents iOS Chrome toolbar overlap
  • -webkit-overflow-scrolling: touch for momentum scrolling
  • Passive touch listeners — no scroll blocking
  • Swipe angle threshold (30°) prevents accidental horizontal/vertical confusion
  • Touch sensitivity configurable via touchSensitivity option
  • Navigation dots have 44×44px minimum touch target (via ::before)
  • Responsive mode available for small viewports

Browser Support

| Browser | Version | |---|---| | Chrome | 69+ | | Firefox | 68+ | | Safari | 14+ | | Safari iOS | 15.4+ (100svh support) | | Edge | 79+ | | Samsung Internet | 10+ |

Fallback: On browsers without scroll-snap support, natural scrolling is used (graceful degradation).


Folder Structure

fullpage/
├── js/
│   ├── core/
│   │   ├── constants.js     — CSS classes, events, keys, thresholds
│   │   ├── config.js        — Default config + merge helpers
│   │   └── state.js         — Reactive state store
│   │
│   ├── modules/
│   │   ├── touch.js         — Swipe gesture detection
│   │   ├── keyboard.js      — Arrow key navigation
│   │   ├── wheel.js         — Mouse wheel handling
│   │   ├── navigation.js    — Dot nav + progress bar
│   │   ├── slides.js        — Horizontal slides
│   │   ├── lazyload.js      — IntersectionObserver lazy loader
│   │   └── plugins.js       — Plugin system + GSAP/Lenis plugins
│   │
│   ├── utils/
│   │   ├── dom.js           — Zero-thrash DOM helpers
│   │   ├── performance.js   — Throttle, debounce, AEM detection
│   │   └── url.js           — Hash management
│   │
│   ├── observers/
│   │   └── observers.js     — IO, ResizeObserver, MutationObserver
│   │
│   ├── accessibility/
│   │   └── accessibility.js — A11y module: ARIA, focus, announcements
│   │
│   ├── FullPageEngine.js    — Main engine class (orchestrator)
│   └── init.js              — Public entry point + auto-init
│
├── scss/
│   ├── base/
│   │   ├── _variables.scss  — Design tokens
│   │   └── _reset.scss      — Base reset
│   ├── components/
│   │   ├── _layout.scss     — Wrapper + section styles
│   │   ├── _navigation.scss — Dot nav + progress
│   │   └── _slides.scss     — Horizontal slides
│   ├── utilities/
│   │   └── _utilities.scss  — Lazy, animations, a11y utilities
│   └── fullpage.scss        — Main SCSS entry
│
├── demo.html                — Full feature demo
└── README.md

License

MIT © FullPage Engine Contributors