Smooth scrolling an element into a specific position within a scrollable container, with custom easing, abort support, and promise-based completion tracking.
- Lightweight — built on top of easing-scroll
- TypeScript-first — written in TypeScript, ships type declarations
- Dual package — ESM and CJS builds
- Positioning — align to
start,center,end, orneareston both axes - Offset — compensate for sticky headers/footers with the
offsetoption - Customizable — bring your own easing function
- Cancellable — abort with AbortSignal
- Promise-based —
awaitcompletion or track partial progress
npm install scroll-into-areapnpm add scroll-into-areaimport { scrollIntoArea } from "scroll-into-area";
const container = document.querySelector("ul");
const target = container.querySelector("li");
await scrollIntoArea(target, {
container,
y: "center",
duration: 400,
easing: (x) => 1 - Math.pow(1 - x, 3), // easeOutCubic
});Smoothly scrolls target into a specific position within the scrollable container.
Type: Element
The DOM element to scroll into view.
| Option | Type | Default | Description |
|---|---|---|---|
container |
HTMLElement |
— | The scrollable container element (required) |
x |
Position |
— | Horizontal alignment: "start", "center", "end", or "nearest" |
y |
Position |
— | Vertical alignment: "start", "center", "end", or "nearest" |
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 |
offset |
number |
0 |
Offset in pixels from the alignment edge (useful for sticky headers/footers) |
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 omitted, the element scrolls instantly and resolves1. - No-op — when both
xandyare omitted, resolves1immediately. - Already-aborted signal — resolves
0without scrolling. - Nearest —
"nearest"only scrolls when the target is outside the visible area. If the target is larger than the container, it aligns tostart. - Offset — creates inward space from the alignment edge. For
"start"and"center", the target is pushed away from the start edge. For"end", the target is pushed away from the end edge. For"nearest", narrows the visible area used for detection.
The default easing is linear (t) => t. Pass any function from easings.net:
await scrollIntoArea(target, {
container,
y: "start",
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 scrollIntoArea(target, {
container,
y: "center",
duration: 400,
signal: controller.signal,
});
if (progress < 1) {
console.log(`Aborted at ${Math.round(progress * 100)}%`);
}A reusable hook that scrolls an element into view and cancels on unmount:
import { useEffect, useRef } from "react";
import { scrollIntoArea, type Position } from "scroll-into-area";
function useScrollIntoArea(
containerRef: React.RefObject<HTMLElement | null>,
targetRef: React.RefObject<Element | null>,
y: Position,
) {
useEffect(() => {
const container = containerRef.current;
const target = targetRef.current;
if (!container || !target) return;
const controller = new AbortController();
scrollIntoArea(target, {
container,
y,
duration: 400,
signal: controller.signal,
easing: (x) => 1 - Math.pow(1 - x, 3),
});
return () => controller.abort();
}, [y]);
}The library exports the Position type for use in your own abstractions:
import { type Position } from "scroll-into-area";
// Position = "start" | "end" | "center" | "nearest"