0%
— IDLE
✓ scroll stopped
↓ loading more…
npm install scroll-snap-kit

Scroll.
v2.0

16 utilities · 5 React hooks · TypeScript types · ESM + CJS + UMD bundles. Zero dependencies.

npm page ↗
16
Utilities
5
React Hooks
3
Bundle formats
0
Dependencies
01 — getScrollPosition() + onScroll()

Live scroll data.

Throttled listener giving you exact position, percentages and direction in real time.

Scroll Y
0px
Progress
0%
Direction
Scroll X
0px
const stop = onScroll(({ y, percentY }) => {
  console.log(`${percentY}% down`)
}, { throttle: 100 })
02 — scrollSpy()

Nav that knows where you are.

The top nav is using it live. Mini-nav below is also synced.

const stop = scrollSpy('section[id]', 'nav a', { offset: 80 })
03 — onScrollEnd()

When scrolling stops.

Fires once after 200ms of idle scroll. Great for lazy-loading or analytics.

Scroll, then stop.
Fires after 200ms idle. Count: 0
const stop = onScrollEnd(() => saveState(), { delay: 200 })
04 — scrollIntoViewIfNeeded()

Only scroll when necessary.

Click a card — if the target is already visible, nothing happens.

📍
Position
scrollIntoViewIfNeeded()
Reveal
scrollIntoViewIfNeeded()
🪤
Trap
scrollIntoViewIfNeeded()
🚀
Hero
scrollIntoViewIfNeeded()
Click a card above.
05 — easeScroll() + Easings

Custom easing curves.

Pick a curve — watch the ball animate across the track.

easeInOutCubic
smooth in & out
easeOutElastic
springy overshoot
easeOutBounce
bouncy landing
easeOutQuart
fast decelerate
linear
constant speed
easeInQuad
slow start
Active: easeInOutCubic
06 — isInViewport() / useInViewport()

Visibility detection.

Cards animate in as they enter the viewport using IntersectionObserver.

Fast
IntersectionObserver — no scroll listeners
🎯
Precise
Configurable threshold 0–1
🔂
once: true
Trigger only the first time
const [ref, inView] = useInViewport({ threshold: 0.2, once: true })
07 — lockScroll() / unlockScroll()

Freeze the page.

Lock body scroll for modals or drawers. Restores position on unlock.

Status: unlocked
08 — scrollSequence()

Choreographed scroll tours.

Chain multiple animations with pauses between steps. Cancel mid-flight.

Idle
1️⃣
Position
waiting
2️⃣
Parallax
waiting
3️⃣
Reveal
waiting
const { promise, cancel } = scrollSequence([
  { target: '#intro', duration: 600 },
  { target: '#features', duration: 800, pause: 400 },
  { target: '#pricing', easing: Easings.easeOutElastic },
])
cancel() // abort anytime
09 — parallax()

Depth on scroll.

Three layers moving at different speeds. Scroll slowly through this section.

speed: 0.2 — background
speed: 0.6
midground
speed: 1.4 — foreground

↑ Scroll to see layers move at different speeds

BG speed:
0.2 slow
0.5 medium
−0.15 reverse
10 — scrollProgress()

Per-element read progress.

0 when the element enters the viewport, 1 when it fully exits. Scroll the article box.

scroll-snap-kit gives you precise control over every aspect of scroll behaviour. Whether you need smooth navigation, progress tracking, or choreographed animations, the API is minimal and composable.

The scrollProgress utility fires a callback with a normalised 0–1 value as the user scrolls through the target element. Build reading progress bars, reveal animations, or sticky sidebars that respond to content position.

Unlike page-level scroll percentage, scrollProgress is scoped to a single element — independently track multiple articles or components on the same page without interference.

Under the hood it uses getBoundingClientRect on every scroll event, throttled via requestAnimationFrame for smooth performance even on long pages.

Keep scrolling this box to reach 100% ✨

Article progress
0%
const stop = scrollProgress(
  '#article',
  p => bar.style.width =
    `${p * 100}%`
)
11 — snapToSection()

Auto-snap to nearest section.

After scrolling stops, snaps to the nearest section. Try scrolling the box below.

Snapped to: none
01
Section One
02
Section Two
03
Section Three
04
Section Four
05
Section Five
12 — scrollReveal() ✦ new in v2

Scroll-triggered animations.

Elements animate in as they enter the viewport. Pick an effect and click Reset to replay.

🎭
Animated
Enters with effect
🌊
On scroll
Triggered by viewport
⚙️
Configurable
Duration, delay, distance
const destroy = scrollReveal('.card', {
  effect: 'slide-up',
  duration: 600,
  delay: 100,
  once: true,
})
destroy() // reset + cleanup
13 — scrollTimeline() ✦ new in v2

CSS driven by scroll.

Drives CSS custom properties from scroll position. The text below fades, scales, and the background shifts as you scroll through this section.

scroll
timeline
Opacity
1.00
Scale
1.00
Blur
0px
BG glow
0%
const stop = scrollTimeline([
  { property: '--hero-opacity', from: 1, to: 0, scrollStart: 0, scrollEnd: 400 },
  { property: '--nav-blur', from: 0, to: 16, unit: 'px', scrollStart: 0, scrollEnd: 200 },
])
14 — infiniteScroll() ✦ new in v2

Load more on demand.

Fires a callback when the user scrolls near the bottom. Auto-throttled with configurable cooldown. Scroll the feed below to load more items.

Loaded: 6 items
⏳ Loading more items…
const stop = infiniteScroll(async () => {
  const items = await fetchMoreItems()
  appendItems(items)
}, { threshold: 300, cooldown: 500 })
15 — scrollTrap() ✦ new in v2

Contain scroll in a modal.

While the trap is active, scroll is locked to the modal box — the page behind won't move. Handles wheel, touch, and keyboard.

📦 Modal content trap: OFF

This is a scrollable modal. When the trap is active, scrolling inside this box will not leak to the page behind it.

Try it: enable the trap, then scroll here. The background page will stay perfectly still while this content scrolls normally.

scrollTrap handles mouse wheel events, touch scroll, and keyboard arrow keys — all three input methods are intercepted.

When the trap is disabled, scroll behaves normally on both this element and the page.

This is essential for accessible modals, drawers, and overlays where scroll leaking breaks the UX.

Scroll all the way down here ↓

✓ You reached the bottom of the modal!

Trap state: disabled
Scroll this box: normal
Page behind: can scroll
const release = scrollTrap(
  document.querySelector('.modal')
)

// when modal closes:
release()