Programmatic smooth scrolling with custom easing, abort support, and promise-based completion tracking.
- Zero dependencies — ~450 bytes min+gzip
- TypeScript-first — written in TypeScript, ships type declarations
- Dual package — ESM and CJS builds
- Customizable — bring your own easing function
- Cancellable — abort with AbortSignal
- Promise-based —
awaitcompletion or track partial progress - Universal — works with any scrollable
Element
npm install easing-scrollpnpm add easing-scrollimport { easingScroll } from "easing-scroll";
const container = document.querySelector(".container");
await easingScroll(container, {
top: 300,
duration: 400,
easing: (x) => 1 - Math.pow(1 - x, 3), // easeOutCubic
});Smoothly scrolls target to the given position.
Type: Element
Any scrollable DOM element.
| Option | Type | Default | Description |
|---|---|---|---|
top |
number |
— | Target vertical scroll position in pixels |
left |
number |
— | Target horizontal scroll position in pixels |
duration |
number |
0 |
Animation duration in milliseconds |
easing |
(t: number) => number |
(t) => t |
Easing function mapping progress (0–1) to eased value |
signal |
AbortSignal |
— | Signal to cancel the animation |
Resolves with a number between 0 and 1 representing animation progress:
| Value | Meaning |
|---|---|
1 |
Animation completed fully |
0 < x < 1 |
Animation was aborted at x progress |
0 |
Animation never started (signal was already aborted) |
- Instant scroll — when
durationis0or negative, the element scrolls instantly and resolves1. - No-op — when both
topandleftare omitted, resolves1immediately. - Clamping — scroll values are clamped to the element's scrollable range. No visual flash occurs.
- Already-aborted signal — resolves
0without scrolling.
The default easing is linear (t) => t. Pass any function from easings.net:
await easingScroll(element, {
top: 500,
duration: 600,
// https://easings.net/#easeOutCubic
easing: (x) => 1 - Math.pow(1 - x, 3),
});Use an AbortController to cancel an in-flight animation:
const controller = new AbortController();
setTimeout(() => controller.abort(), 100);
const progress = await easingScroll(element, {
top: 1000,
duration: 400,
signal: controller.signal,
});
if (progress < 1) {
console.log(`Aborted at ${Math.round(progress * 100)}%`);
}A reusable hook that cancels the previous scroll when dependencies change or the component unmounts:
import { useEffect, RefObject } from "react";
import { easingScroll } from "easing-scroll";
function useEasingScroll(ref: RefObject<HTMLElement | null>, top: number) {
useEffect(() => {
const target = ref.current;
if (!target) return;
const controller = new AbortController();
easingScroll(target, {
top,
duration: 400,
signal: controller.signal,
easing: (x) => 1 - Math.pow(1 - x, 3),
});
return () => controller.abort();
}, [top]);
}