Full-Screen Scroll
February 11, 2021
overflow-y: hidden
for html and body, and overflow-y: scroll
for the scroller elementwindow.requestAnimationFrame
to set scrollTop
scroll
event. But in mobile devices, we should use touchmove
event.scrollTop
value (or the touch screenY
value in the touchmove
event). During the second time, compare the values to the previous ones.// Add event listener. Or you can bind them to the component directly in Vue or React
document.querySelector(".scroll").addEventListener("scroll", handleScroll);
// Necessary variables for the event callback.
// I placed them here for clarity.
// Place them anywhere that fits the architecture of your project.
let scroller; // the scroller element
let currentY; // the current scrollTop of the scroller
let currentPageY; // the y value of the current page
let scrollDirection; // 1 or -1
let isScrolling;
// The event callback
const handleScroll = (e) => {
// for scroll we don't need the event argument,
// but for touchmove event callback we have to use it for the touch y value
if (!scroller) {
return;
}
currentY = scroller.scrollTop;
// currentY = e.touches[0].screenY; for touchmove
if (!isScrolling && currentY !== currentPageY) {
if (currentY > currentPageY) {
scrollDirection = -1;
}
if (currentY < currentPageY) {
scrollDirection = 1;
}
isScrolling = true;
window.requestAnimationFrame(scrollTo);
}
};
100vh
, then get window.innerHeight
, and we should be good. But for mobile devices, we have to deal with the problem that the browser address bar might hide during scrolling, which causes the visible area height to be inconsistent.<html>
element. Therefore, to avoid URL bar hides during scrolling, it is necessary to prevent root element from scrolling while scrolling the contents.<html>
and <body>
has overflow: auto
. Set both to overflow: hidden
, and we locked up <html>
and <body>
for good.overflow: scroll
. Now when you scroll the viewport, <html>
and <body>
don't move at all, only the scroller is scrolling, and the URL bar will not hide. Notice that if there are any container elements between the scroller and the body, their overflow values also need to be hidden
.html {
height: 100vh;
overflow-y: hidden;
}
body {
height: 100%;
overflow-y: hidden;
}
.scroller {
height: 100%;
overflow-y: scroll;
}
.scrollTo
is initiative and efficient, but it's not very reliable. I've encountered scenarios that it wouldn't work (no effect at all). I haven't discovered the root cause, but it feels like .scrollTo
gets canceled when called too frequently.DOMHighResTimeStamp
, which is essentially a number. When scroll animation starts, save the initial timestamp and then compare the timestamp in each call with the initial timestamp to determine if the elapsed time has exceeded the designated scroll animation duration. If the scroll is not finished yet, call window.requestAnimationFrame
inside the callback recursively.scroller.scrollTop = {the y position after each scroll step}
scroll-behavior: "smooth"
doesn't apply when changing a div's scrollTop
value in this way; this is why we need to use requestAnimationFrame
to create an animation.let scrollDuration;
let scrollDirection; // 1 or -1
let scrollStartTime;
let scroller;
let currentY;
let scrollDistance; // window height, likely 100vh
let isScrolling;
const scrollTo = (timestamp: DOMHighResTimeStamp) => {
if (!scrollStartTime) {
scrollStartTime = timestamp;
}
const elapsedTime = timestamp - scrollStartTime;
const progress = Math.min(elapsedTime / scrollDuration, 1);
scroller.scrollTop = currentY + scrollDistance * progress * scrollDirection;
if (elapsedTime < duration) {
window.requestAnimationFrame(scrollTo);
} else {
currentY = scroller.scrollTop;
scrollStartTime = undefined;
scrollDirection = undefined;
isScrolling = false;
}
};
scroll
or touchmove
event, it's unlikely that the event only fires once. We can have the conditioning to prevent our full-screen scroll from repeatedly called, but the other scrolls caused by the user's finger remains, even after the full-screen scroll finishes. Removing the scroll momentum is the necessary polishing for the full-screen scroll effect.pointerEvent: none
, and —you might have guessed it— overflow: hidden
. I found overflow more reliable, for pointerEvent
didn't work in some touchmove
scenario.window.requestAnimationFrame
for the first time), add the above CSS property to the scroller element, and all the scrolls that after the first callback should take no effect. Thus we removed the momentum successfully.