@elementmints/snapscroll
v2.1.2
Published
Production-grade, accessibility-first fullpage scrolling engine. CSS Scroll Snap. Zero dependencies.
Maintainers
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.
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 directlyQuick 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 cleanupCustom 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:
GranitenamespaceCQnamespace- 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-labelfrom 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-motiondisables all animations - ✅ Navigation dots have proper
aria-labelandaria-current - ✅ Slide arrows have
aria-label - ✅ Focus trapping respects overflow sections
- ✅ All interactive elements have
:focus-visiblestyles
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 100svhpreserves layout while content-visibility isauto- No DOM mutations during scroll animations
- Image dimensions required via
width/heightattributes
INP
- All event handlers use
{ passive: true } - No
preventDefaulton wheel/touch - Keyboard handler is lightweight (one
keydownlistener, 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
transformandopacityare animated (no layout, no paint) will-change: contentsapplied only to active sectioncontain: layout painton all sectionscontent-visibility: autofor off-screen paint elimination
Mobile Optimization
100svh(small viewport height) prevents iOS Chrome toolbar overlap-webkit-overflow-scrolling: touchfor momentum scrolling- Passive touch listeners — no scroll blocking
- Swipe angle threshold (30°) prevents accidental horizontal/vertical confusion
- Touch sensitivity configurable via
touchSensitivityoption - 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.mdLicense
MIT © FullPage Engine Contributors
