Skip to content

Instantly share code, notes, and snippets.

@guillermodlpa
Last active June 13, 2023 10:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guillermodlpa/b31c7a00ce50465bb44b034e7231d794 to your computer and use it in GitHub Desktop.
Save guillermodlpa/b31c7a00ce50465bb44b034e7231d794 to your computer and use it in GitHub Desktop.
smooth scroll function. native scrollTo or scrollIntoView conflict with each other if there are different horizontal and vertical scrolls happening
/**
* Inspired by https://codepen.io/oxleberry/pen/BOEBaB
*
* With added support for horizontal scrolling, scrolling the window, and linear easing
*
* native scrollTo or scrollIntoView conflict with each other if there are different horizontal and vertical scrolls happening
*/
// Easing equations, http://www.gizma.com/easing/
function easeOutCubic(t: number, b: number, c: number, d: number) {
t /= d;
t--;
return c * (t * t * t + 1) + b;
}
// function linear(t: number, b: number, c: number, d: number) {
// return (c * t) / d + b;
// }
type SmoothScrollToOptions = {
targetPos: number;
horizontal?: boolean;
duration?: number;
getOffsetParent: () => Element | null;
};
export function smoothScrollToPosition({
targetPos,
horizontal = false,
duration = 250,
getOffsetParent,
}: SmoothScrollToOptions) {
let offsetParent = getOffsetParent();
if (!offsetParent) return;
if (offsetParent.tagName === 'BODY') {
offsetParent = document.getElementsByTagName('html')[0];
if (!offsetParent) return;
}
// tracks the current X position in pixels
const currentPos = horizontal
? offsetParent.scrollLeft
: offsetParent.scrollTop;
// tracks the remaining distance from target in pixels
const distance = targetPos - currentPos;
// track the time, for use with request animation
let start: null | number = null;
let animationId: number | undefined = undefined;
const animation = (timestamp: number) => {
// timestamp part of reqAF to keep track of animation time
if (!start) start = timestamp;
// tracks the time elapsed
const timeElapsed = timestamp - start;
// run, calculates how to reach targetPos in an ease trajectory
// 1) value of current time in the animation
// 2) currentPos position in pixel
// 3) how far we need to go till we reach targetPos in pixels
// 4) target end time of animation
const run = easeOutCubic(timeElapsed, currentPos, distance, duration);
// scrollTo, first argument scrolls on the x axis
// scrollTo, second argument scrolls on the y axis
// animates till we reach the duration time
offsetParent?.scrollTo(horizontal ? { left: run } : { top: run });
if (timeElapsed < duration) {
requestAnimationFrame(animation);
} else if (animationId) {
cancelAnimationFrame(animationId);
}
};
// recursively renders the animation function
animationId = requestAnimationFrame(animation);
}
export type SmoothScrollOptions = {
horizontal?: boolean;
duration?: number;
getTargetPos?: (target: HTMLElement, offsetParent: Element) => number;
getOffsetParent?: (target: HTMLElement) => Element | null;
};
export function smoothScrollToTarget(
target: HTMLElement,
{
horizontal = false,
duration = 250,
getTargetPos,
getOffsetParent,
}: SmoothScrollOptions = {},
) {
let offsetParent = getOffsetParent
? getOffsetParent(target)
: target.offsetParent;
if (!offsetParent) return;
if (offsetParent.tagName === 'BODY') {
offsetParent = document.getElementsByTagName('html')[0];
if (!offsetParent) return;
}
// tracks the target X positiom in pixels
const targetPos = getTargetPos
? getTargetPos(target, offsetParent)
: horizontal
? target.offsetLeft
: target.offsetTop;
return smoothScrollToPosition({
targetPos,
horizontal,
duration,
getOffsetParent: () => offsetParent,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment