scroll-utils
v1.0.1
Published
A super lightweight scroll utility library built for React applications.
Maintainers
Readme
Scroll Utils
A lightweight, dependency-free set of scroll utilities for React and vanilla web apps — positional scrolling, scroll-to-element, SSR-safe helpers, and a ref-counted scroll-locking mechanism, all in a ~1.7 KB bundle.
Table of Contents
- Features
- Installation
- Getting Started
- API Reference
- Type Reference
- React Usage Examples
- SSR / Next.js Support
- Accessibility
- License
Features
- Tiny — ~1.7 KB minified, zero runtime dependencies
- Positional scrolling — scroll to
top,bottom,left, orrightwith optional offsets - Scroll-to-element — scroll to any element by ID, with automatic focus management for accessibility
- React-friendly — handler factories built for
onClick/ event-based usage - Reduced-motion aware — automatically switches to instant scrolling when the user prefers reduced motion
- Scroll locking — ref-counted, queue-based scroll lock/unlock with automatic scrollbar-gutter compensation and an iOS Safari
touchmovefix - SSR-safe — every function no-ops gracefully when
windowis undefined - Fully typed — ships with complete TypeScript definitions
Installation
npm install scroll-utilsyarn add scroll-utilspnpm add scroll-utilsGetting Started
import { scrollToTop, scrollToId } from 'scroll-utils';
// Scroll the window to the top
scrollToTop();
// Scroll to an element by ID
scrollToId({ id: 'section-2', behavior: 'smooth' });API Reference
scroll(settings: ScrollSettings): void
The core positional scroll function. All directional helpers below are thin wrappers around this.
| Param | Type | Default | Description |
| -------------------- | -------------------------------- | --------------------- | ------------------------------------------------------------- |
| settings.position | ScrollPosition | 'top' | Direction to scroll: 'top', 'bottom', 'left', 'right' |
| settings.offset | ScrollOffset | { top: 0, left: 0 } | Extra offset applied to the computed scroll position |
| settings.container | HTMLElement \| Window | window | Element to scroll instead of the window |
| settings.behavior | ScrollBehavior | 'smooth' | 'auto', 'smooth', or 'instant' |
| settings.event | React.MouseEvent \| MouseEvent | — | If provided, preventDefault() is called on it |
scroll({
position: 'right',
offset: { top: 100, left: 50 },
container: document.getElementById('scrollable-container'),
behavior: 'smooth',
});If the user's OS has "reduce motion" enabled,
behavioris automatically overridden to'instant'.
scrollToTop / scrollToBottom / scrollToLeft / scrollToRight
Convenience wrappers around scroll() that lock in the position for you. Each accepts the same settings minus position.
scrollToTop({ offset: { top: 10 } });
scrollToBottom({ container: myContainerEl });
scrollToLeft();
scrollToRight({ behavior: 'auto' });scrollToId(settings: ScrollToIdSettings): void
Scrolls to a specific element by its ID (with or without a leading #). When scrolling within a custom container, the target element is also focused (with tabindex="-1" applied if needed) for keyboard/screen-reader accessibility.
| Param | Type | Default | Description |
| -------------------- | -------------------------------- | --------------------- | --------------------------------------------------------------- |
| settings.id | string | '' | The target element's ID, e.g. 'my-section' or '#my-section' |
| settings.offset | ScrollOffset | { top: 0, left: 0 } | Extra offset applied to the computed scroll position |
| settings.container | HTMLElement \| Window | window | Element to scroll instead of the window |
| settings.behavior | ScrollBehavior | 'smooth' | 'auto', 'smooth', or 'instant' |
| settings.event | React.MouseEvent \| MouseEvent | — | If provided, preventDefault() is called on it |
scrollToId({
id: 'pricing',
offset: { top: 20, left: 0 },
behavior: 'smooth',
});If the element isn't found, a console.warn is emitted and the function returns without throwing.
createScrollHandler(settings: ScrollSettings)
Returns a ready-to-use event handler for onClick (or any DOM event) that triggers scroll() with the given settings.
const handleScrollTop = createScrollHandler({ position: 'top' });
<button onClick={handleScrollTop}>Back to top</button>createScrollToIdHandler(settings: ScrollToIdSettings)
Same idea as createScrollHandler, but for scrollToId().
const handleScrollToPricing = createScrollToIdHandler({ id: 'pricing' });
<button onClick={handleScrollToPricing}>View pricing</button>lockScroll / unlockScroll
lockScroll(container?: HTMLElement): Promise<void>
unlockScroll(container?: HTMLElement): Promise<void>Lock or unlock scrolling on a given container (defaults to document.documentElement, i.e. the whole page). Calls are queued internally, so rapid lock/unlock calls always execute in order and never race each other.
import { lockScroll, unlockScroll } from 'scroll-utils';
// Lock the whole page (e.g. when opening a modal)
await lockScroll();
// ...later, when the modal closes
await unlockScroll();
// Or lock a specific scrollable container
const drawer = document.getElementById('drawer');
await lockScroll(drawer);
await unlockScroll(drawer);Locking is ref-counted. If lockScroll() is called twice on the same container (e.g. two modals stacked), the container only unlocks once unlockScroll() has also been called twice — matching lock/unlock calls one-to-one.
What locking does under the hood:
- Sets
overflow: hiddenon the container - Compensates for scrollbar removal by adjusting
padding-right(skipped ifscrollbar-gutter: stableis already set) - For the document element specifically, pins it with
position: fixedand restores the exact scroll offset on unlock - Adds a
touchmovelistener to prevent iOS Safari's rubber-band scroll-through - Sets a
data-scroll-locked="true"attribute on the locked element (handy for CSS hooks) - Restores all original inline styles on unlock
Type Reference
type ScrollPosition = 'top' | 'bottom' | 'left' | 'right';
type ScrollBehavior = 'auto' | 'smooth' | 'instant';
interface ScrollOffset {
top: number;
left: number;
}
interface ScrollSettings {
position: ScrollPosition;
offset?: ScrollOffset;
container?: HTMLElement | Window;
behavior?: ScrollBehavior;
event?: React.MouseEvent | MouseEvent;
}
interface ScrollToIdSettings {
id: string;
offset?: ScrollOffset;
container?: HTMLElement | Window;
behavior?: ScrollBehavior;
event?: React.MouseEvent | MouseEvent;
}
interface ScrollLockMetadata {
count: number;
scrollTop: number;
originalStyle: {
overflow: string;
position: string;
top: string;
width: string;
paddingRight: string;
};
}All types are exported from the package root and can be imported directly:
import type { ScrollSettings, ScrollToIdSettings } from 'scroll-utils';React Usage Examples
Scroll-to-top button:
import { createScrollHandler } from 'scroll-utils';
function BackToTopButton() {
return (
<button onClick={createScrollHandler({ position: 'top', behavior: 'smooth' })}>
↑ Back to top
</button>
);
}Anchor-style navigation:
import { createScrollToIdHandler } from 'scroll-utils';
function NavLink({ id, label }: { id: string; label: string }) {
return (
<a href={`#${id}`} onClick={createScrollToIdHandler({ id, offset: { top: 64, left: 0 } })}>
{label}
</a>
);
}Modal with scroll lock:
import { useEffect } from 'react';
import { lockScroll, unlockScroll } from 'scroll-utils';
function Modal({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) {
useEffect(() => {
if (!isOpen) return;
lockScroll();
return () => {
unlockScroll();
};
}, [isOpen]);
if (!isOpen) return null;
return <div className="modal">{children}</div>;
}SSR / Next.js Support
Every function checks for window before touching the DOM and safely no-ops during server-side rendering — no extra guards needed in your components. isWindowUndefined() is used internally for this check.
Accessibility
scroll()andscrollToId()respectprefers-reduced-motionand automatically fall back to instant scrolling.scrollToId()moves focus to the target element when scrolling within a custom container, so keyboard and screen-reader users land in the right place.- Scroll locking preserves and restores the original scroll position and styles exactly, avoiding layout shift when a modal or drawer closes.
License
Apache-2.0 © 2026-present Suryansh Singh
