Scroll
import { lockScroll, scrollPosition, scrollTo } from '@studiometa/js-toolkit-v4/utils';scrollPosition and scrollTo
scrollPosition(target: ScrollToTarget, options?: ScrollPositionOptions): ScrollPosition
scrollTo(target: ScrollToTarget, options?: ScrollToOptions): ScrollPositionThe two halves are separate because callers need them separately. scrollPosition() measures and returns; scrollTo() calls it and moves.
A carousel asks which slide is nearest three times for every time it travels — and in v3, asking meant scrolling.
import { scrollPosition, scrollTo } from '@studiometa/js-toolkit-v4/utils';
const el = document.body;
// Where would we end up?
const { top, left } = scrollPosition(el, { align: 'center' });
// Go there.
scrollTo(el, { align: 'center', offset: -80 });
scrollTo('#section', { axis: 'y' });
scrollTo(0);
scrollTo({ top: 200 });The target
type ScrollToTarget = string | Element | number | Partial<{ left: number; top: number }>;A selector, an element, a number, or a position. A number or a position names its own destination, so align does not apply to it.
The options
interface ScrollPositionOptions {
rootElement?: Element | Window; // what scrolls. Defaults to the window.
axis?: 'x' | 'y' | 'both'; // which axes a target that names none may move. Defaults to 'y'.
offset?: number; // pixels to stop short. Defaults to 0.
align?: ScrollAlign | { x?: ScrollAlign; y?: ScrollAlign };
}
interface ScrollToOptions extends ScrollPositionOptions {
behavior?: ScrollBehavior; // 'smooth', or 'instant' when the reader asked for less motion
}SCROLL_AXES and SCROLL_ALIGNMENTS are the frozen sets behind axis and align.
align is 'start' | 'center' | 'end', or one per axis, and it applies to an element target only.
The names are physical, like axis
x and y, not the platform's inline and block. Nothing here maps a writing mode, and borrowing that vocabulary without the mapping would promise what compute-scroll-into-view promises and does not deliver.
What the arithmetic gets right
- The viewport is the client box, so a scrollbar gutter is out of the arithmetic with no special case.
- The destination is clamped to the scroll range: centring the first slide asks for a negative offset and gets
0. behaviordefaults to'smooth', or to'instant'when the reader has asked for less motion — so honouring the preference is the default rather than a call site's responsibility.
A dependency was measured and refused
compute-scroll-into-view is 1.4 kB and walks every scrolling ancestor — which is what v4's single rootElement contract declines, and what a boundary option already cancels. Its own source leaves writing modes unimplemented and reads no scroll-padding, so the real delta over core was about twenty lines.
lockScroll
lockScroll(target?: HTMLElement): () => voidimport { lockScroll } from '@studiometa/js-toolkit-v4/utils';
const release = lockScroll();
// … the modal is open
release();It counts. A modal surface is not alone on a page: a dialog opened from inside a drawer is two holders, and the one that closes first must not put the scroll back under the one still open.
- The first lock saves the inline value it found.
- The last release puts exactly that value back.
- The ones between only move the count.
The release is idempotent, so a surface calls it on close and again on unmount without counting twice — and a component unmounted while open owes the page its scroll, which is what the second call is for.
mounted() {
return this.release ?? undefined;
}The count is shared across evaluated copies of the package, through the same runtime slot the focus helpers use, for the same reason: there is one scroll per document.
It is overflow: hidden and nothing else
No paddingRight compensation. scrollbar-gutter: stable is the page's own answer and it does not mis-handle fixed children.
iOS Safari remains unreliable — which is the argument for having one function rather than a copy per component.
A native <dialog> needs it too
showModal() gives the top layer, the backdrop, a focus trap and Escape. It does not stop the page behind it scrolling.
See also
useScroll()— scroll position as a serviceuseScrollProgress()— an element's progress through the viewport