scroll-into-view-promise
v1.0.0
Published
Promise-based scrollIntoView. Wraps the native smooth-scroll and resolves when the animation finishes. Tiny. Typed. Zero dependencies.
Maintainers
Readme
🎯 scroll-into-view-promise
Promise-based
scrollIntoView. Wraps the native smooth-scroll and resolves when the animation finishes. Zero dependencies.
Why
element.scrollIntoView({ behavior: 'smooth' }) works great — but it has no callback. You can't await it. You can't know when it finishes. This is one of the most-searched problems on StackOverflow and has been an open issue in both TC39 and the W3C CSS WG for years.
Existing npm packages like scroll-into-view are old and large. scroll-into-view-if-needed is powerful but solves a different problem. There's no tiny, modern wrapper that simply gives you a Promise around the native API.
scroll-into-view-promise is that wrapper. ~0.6kB gzipped. Zero dependencies. Returns a Promise that resolves when the scroll finishes.
Install
npm install scroll-into-view-promise
# or
yarn add scroll-into-view-promise
# or
pnpm add scroll-into-view-promiseQuick Start
import scrollIntoView from 'scroll-into-view-promise';
const section = document.getElementById('pricing');
// Await the scroll — runs code AFTER the animation finishes
await scrollIntoView(section);
console.log('Scroll complete!');
// With options
await scrollIntoView(section, { block: 'center', timeout: 5000 });Features
- Promise-based —
awaitthe native smooth scroll, run code when it finishes - Zero dependencies — pure TypeScript, no external packages
- Uses the native API — calls
element.scrollIntoView()under the hood, not a custom scroll implementation - Scroll-idle detection — resolves by listening for scroll-end on all scrollable ancestors
- Timeout safety net — never hangs; resolves after a configurable timeout (default 3s)
- AbortController support — cancel a pending scroll with
signal - Handles nested scrolling — detects all scrollable parent containers automatically
- Instant fallback — if
behavior: 'instant'or smooth scroll is unsupported, resolves immediately - SSR safe — guards DOM access; safe to import in Node/SSR environments
- ~0.6kB minified + gzipped
API
scrollIntoView(element, options?)
Returns Promise<void> that resolves when the scroll animation finishes.
| Param | Type | Description |
|-------|------|-------------|
| element | Element | The DOM element to scroll into view |
| options | ScrollIntoViewPromiseOptions | Optional configuration (see below) |
ScrollIntoViewPromiseOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| behavior | 'smooth' \| 'instant' \| 'auto' | 'smooth' | Scroll behavior (passed to native API) |
| block | 'start' \| 'center' \| 'end' \| 'nearest' | 'start' | Vertical alignment |
| inline | 'start' \| 'center' \| 'end' \| 'nearest' | 'nearest' | Horizontal alignment |
| timeout | number | 3000 | Max wait time in ms before resolving anyway |
| signal | AbortSignal | — | Abort signal to cancel and reject the Promise |
Examples
Sequential scrolls
import scrollIntoView from 'scroll-into-view-promise';
async function walkthrough() {
await scrollIntoView(document.getElementById('step-1'));
showTooltip('step-1');
await scrollIntoView(document.getElementById('step-2'));
showTooltip('step-2');
await scrollIntoView(document.getElementById('step-3'));
showTooltip('step-3');
}Focus after scroll
const input = document.getElementById('email-input');
await scrollIntoView(input, { block: 'center' });
input.focus(); // guaranteed to be visibleCancel with AbortController
const controller = new AbortController();
scrollIntoView(target, { signal: controller.signal })
.catch((err) => {
if (err.name === 'AbortError') console.log('Scroll cancelled');
});
// Cancel after 500ms
setTimeout(() => controller.abort(), 500);React hook
import { useCallback } from 'react';
import scrollIntoView from 'scroll-into-view-promise';
function useScrollTo() {
return useCallback(async (selector: string) => {
const el = document.querySelector(selector);
if (el) {
await scrollIntoView(el, { block: 'center' });
}
}, []);
}
// Usage
function App() {
const scrollTo = useScrollTo();
return (
<button onClick={() => scrollTo('#pricing').then(() => alert('Done!'))}>
Jump to Pricing
</button>
);
}Vue composable
import scrollIntoView from 'scroll-into-view-promise';
export function useScrollTo() {
async function scrollTo(selector: string) {
const el = document.querySelector(selector);
if (el) await scrollIntoView(el, { block: 'center' });
}
return { scrollTo };
}CDN (no build step)
<script type="module">
import scrollIntoView from 'https://esm.sh/scroll-into-view-promise';
document.getElementById('go-btn').addEventListener('click', async () => {
await scrollIntoView(document.getElementById('target'));
console.log('Arrived!');
});
</script>