I'm writing a virtual scrolling lib and encounter a weird problem which only appears in chrome and disable smooth scrolling in chrome flag fix it. One part of the logic is to use an invisible div to stretch the container and when scroll to top, fetch some new data then scroll back to old top. However, in chrome with smooth scrolling, it seems that changes to scrollTop will not be applied immediately so multiple data is fetched and scroll position is wrong.
I'm using chrome 84.0.4147.105-1 on Linux and have tested on Windows with same result. It may sometimes be hard to trigger, the safest way is to drag the scrollbar near top without trigger fetching and scroll up. Then you shall find console value jump from 0 to 900(scroll down) to a small number(why?) and trigger fetch again
const container = document.getElementById('container')
const scroller = document.getElementById('scroller')
let height = 0
let isFetching = false
const fetchData = h => {
if (isFetching) return
isFetching = true
setTimeout(() => {
height += h
scroller.style.height = height + 'px'
container.scrollTop += h
isFetching = false
}, 50)
}
fetchData(800)
container.addEventListener('scroll', ev => {
const top = ev.target.scrollTop
console.log(top)
if (top <= 50) {
fetchData(800)
}
})
#container {
height: 200px;
width: 500px;
background-color: blue;
overflow: auto;
}
#scroller {
width: 100%;
}
<div id="container">
<div id="scroller"></div>
</div>
A temporary (and really inelegant) fix is changing fetchData to
const fetchData = h => {
if (isFetching) return
isFetching = true
setTimeout(() => {
height += h
scroller.style.height = height + 'px'
container.scrollTop += h
isFetching = false
setTimeout(() => container.scrollTop += h, 200)
}, 50)
}
which scrolls again after a small delay
Related
On my simple website I have a horizontal navigation bar with several button there. When I scroll down I want it to be pinned on top.
For that I use a JS script which changes display type of the navigation bar from position:relative to position:fixed at a certain moment.
But the problem occurs with the website contents following the nav bar. Since elements with position:fixed are removed from the DOM flow and there is no created space for them, following contents displace from their position consequently being overflowed by the nav bar.
My navigation bar might change its shape due to wrap. It might also be moved down the page since upper horizontal elements may wrap too.
What the right way to do it? Is there any way to create space in the DOM for elements with position:fixed?
Main CSS:
main {
padding-top: 0;
margin-left: 20px;
margin-top: 0;
}
Navigation bar CSS:
.layout-nav {
display: flex;
flex-flow: wrap;
position: relative;
top: 0;
width: 100%;
padding: 10px 0 0;
margin-left: 10px;
background: #424242;
}
The JS script with a method which changes display type and padding on a certain scrollY which vary depending on the window width which leads to certain wrap:
let lastScrollY = 0;
let lastWidth = window.innerWidth;
let maxScrollPos = 105;
let ticking = false;
let fixed = false;
function modifyTitle(scrollPos, width) {
if (width > 773) {
maxScrollPos = 105;
} else {
maxScrollPos = 164;
}
if (scrollPos > maxScrollPos && !fixed) {
const topNav = document.querySelector('.layout-nav');
const main = document.querySelector('main');
topNav.style.position = "fixed";
topNav.style.padding = "23px 0 0";
main.style.paddingTop = "70px";
fixed = true;
} else if (scrollPos <= maxScrollPos && fixed) {
const topNav = document.querySelector('.layout-nav');
const main = document.querySelector('main');
topNav.style.position = "relative";
topNav.style.padding = "10px 0 0";
main.style.paddingTop = "0";
fixed = false;
}
}
document.addEventListener('scroll', (e) => {
lastScrollY = window.scrollY;
lastWidth = window.innerWidth;
if (!ticking) {
window.requestAnimationFrame(() => {
modifyTitle(lastScrollY, lastWidth);
ticking = false;
});
ticking = true;
}
});
So as of now I hardcoded some cases of wraps so that after navigation bar being pinned following content would be shown properly.
It is hard to hardcode every case.
And this solution seems not right at all for me since it is based not just on scrollY of the page but also on the width of the windows which indirectly causes certain wraps based on the resolution.
I am trying to copy an open curtain animation like in the following where the side divs expand horizontally on the background image on scroll.
I have a working example with the following code
import { useState, useEffect, useRef, useCallback } from "react";
import "./styles.css";
export default function App() {
// check window scroll direction
const [y, setY] = useState(null);
const [scrollDirection, setScrollDirection] = useState("");
const boxTwo = useRef(null);
const boxTwoLeft = useRef(null);
const boxTwoRight = useRef(null);
const countRefTranslateX = useRef(0);
// check window scroll direction https://stackoverflow.com/questions/62497110/detect-scroll-direction-in-react-js
const handleScrollDirection = useCallback(
(e) => {
const window = e.currentTarget;
if (y > window.scrollY) {
setScrollDirection("up");
} else if (y < window.scrollY) {
setScrollDirection("down");
}
setY(window.scrollY);
},
[y]
);
const handleScroll = useCallback(() => {
if (boxTwo.current) {
let position = boxTwo.current.getBoundingClientRect();
// checking for partial visibility and if 50 pixels of the element is visible in viewport
if (
position.top + 50 < window.innerHeight &&
position.bottom >= 0 &&
scrollDirection === "down"
) {
countRefTranslateX.current = countRefTranslateX.current + 3;
boxTwoLeft.current.style.transform = `translateX(-${countRefTranslateX.current}px)`;
boxTwoRight.current.style.transform = `translateX(${countRefTranslateX.current}px)`;
} else if (
position.top + 50 < window.innerHeight &&
position.bottom >= 0 &&
scrollDirection === "up"
) {
countRefTranslateX.current = countRefTranslateX.current - 3;
boxTwoLeft.current.style.transform = `translateX(-${countRefTranslateX.current}px)`;
boxTwoRight.current.style.transform = `translateX(${countRefTranslateX.current}px)`;
} else {
countRefTranslateX.current = 0;
boxTwoLeft.current.style.transform = `translateX(-${countRefTranslateX.current}px)`;
boxTwoRight.current.style.transform = `translateX(${countRefTranslateX.current}px)`;
}
}
}, [scrollDirection]);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleScrollDirection);
return () => {
window.removeEventListener("scroll", handleScrollDirection);
};
}, [handleScrollDirection]);
return (
<div className="App">
<div className="boxOne"></div>
<div ref={boxTwo} className="boxTwo">
<div ref={boxTwoLeft} className="boxTwoLeft"></div>
<div ref={boxTwoRight} className="boxTwoRight"></div>
</div>
<div className="boxThree"></div>
</div>
);
}
I have two issues.
My right white div keeps going to the left while I scroll up
I cannot get a fixed vertical window breakpoint. My animation continues after the window has scrolled after the point I want to start/stop moving the divs
How can I resolve these issues?
My codesandbox
I've looked over what you've done but it's way too complicated.
All you want is to place two curtains (panels) on top of your content.
The container should have position:relative and the curtains should have position: absolute.
You then declare the scrollLimits in between which they should move. In the example below, 0 is the starting point and window.innerHeight the end point. Replace those values with whatever makes sense for your section, considering its vertical position in the page. You could use the section's current offsetTop and clientHeight to set the limits dynamically, based on current window size.
You then get the current scroll position and calculate the scroll percentage relative to the limits.
You then apply the percentage/2 + 50% to each curtain's transform: translateX() the left one with negative value, the right one with positive value.
Done.
document.addEventListener('scroll', revealSection);
/* function revealSection() {
const scrollLimits = [0, window.innerHeight];
const currentScroll = window.scrollY;
const percent = Math.min(currentScroll * 100 / (scrollLimits[1] - scrollLimits[0]), 100);
document.querySelector('.l-panel').style.transform = `translateX(-${percent/2 + 50}%)`;
document.querySelector('.r-panel').style.transform = `translateX(${percent/2 + 50}%)`;
} */
function revealSection() {
const top = document.querySelector('section').getBoundingClientRect().top;
const percent = Math.min(Math.max(0, (window.innerHeight - top) / window.innerHeight * 100), 100);
document.querySelector('.l-panel').style.transform = `translateX(-${percent/2 + 50}%)`;
document.querySelector('.r-panel').style.transform = `translateX(${percent/2 + 50}%)`;
}
body {
margin: 0;
}
section {
background-image: url('https://static.toss.im/assets/homepage/new tossim/section2_4_big.jpg');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
width: 100%;
height: 100vh;
margin: 100vh 0;
position: relative;
overflow: hidden;
}
.l-panel, .r-panel{
position: absolute;
height: 100%;
width: 100%;
content: '';
top: 0;
background-color: white;
left: 0;
}
<section>
<div class="l-panel"></div>
<div class="r-panel"></div>
</section>
Note: change the CSS selectors so the styles apply to your section only.
Edit: I've come up with a more generic way to set the scroll interval, using the section's getBoundingClientRect().top and window.innerHeight. It's probably more useful, as you no longer have to worry about the section's position in page.
I've left the previous version in, for reference and/or for anyone who prefers it.
I am trying to recreate the Monocle ereader. As best I understand, it works by columnating the page and then scrolling horizontally across the columns.
I have the following HTML and Javascript:
body {
column-width: 100vw;
max-height: calc(100vh - 30px);
box-sizing: border-box;
}
window.addEventListener(
'wheel',
(evt) => {
const delta = evt.deltaX + evt.deltaY
const pages = Math.round(Math.abs(delta) / delta)
const curr = Math.round(window.scrollX / window.innerWidth)
window.scroll((curr + pages) * window.innerWidth, 0)
}
)
As this pen shows when you scroll, as the pages progress, the text drifts to the left.
It looks like it's drifting due to the vertical scrollbar on the right. For me, adding overflow-y: hidden; to the body's css to hide the scrollbar seemed to do the trick.
If you need to have the scrollbar there, you might need to calculate the scrollbar width and add/subtract it to compensate. More info on doing that here: https://stackoverflow.com/a/13382873/6184972
Poor browser has to repaint for every instance of a 'wheel' event! Try console logging for each wheel event, you'll see it's pretty frequent! Try reducing the number of times the browser has to repaint. It'll be a balancing act between a smooth scroll and a better performance:
let counter = 0;
const buffer = 3; // I made this a variable so you can easily see what it is and change it
window.addEventListener(
'wheel',
(evt) => {
const delta = evt.deltaX + evt.deltaY
const pages = Math.round(Math.abs(delta) / delta)
const curr = Math.round(window.scrollX / window.innerWidth)
let scrollAmount = (curr + pages) * window.innerWidth
if (scrollAmount >= (counter + buffer)) {
window.scroll(scrollAmount, 0)
counter = scrollAmount
}
}
)
I am trying to create a scrolling animation with 2 divs and 2 images.
For lack of a better explanation (as you might have guessed from the title) I have made a quick animation showcasing what I am trying to achieve.
here is a hosted version that I made earlier. I tried to create the effect with the help of parallax scrolling, but it's not quite what I want.
It's a Zeit Now deployment, so you can append /_src to the url and take a look at the source code.
Now I am not sure if this is even the correct way to create the animation and to be honest I wouldn't know any other way that I could approach this.
So I am not asking for a fully-fledged answer without any flaws (although it would be much appreciated), but rather a nudge in the right direction.
Made this quickly so there might be some issues, I tried to make the variables somehow general so you can play with things (check this fiddle)
const body = document.body,
html = document.documentElement;
const targetImg = document.querySelector('.second');
// our image's initial height
const imgHeight = targetImg.clientHeight;
// the final value for image height (at scroll end)
const imgTargetHeight = 0;
// total height of our document
const totalHeight = Math.max(body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);
// visible window height
const windowHeight = window.innerHeight;
// starting scroll position we want to start calculations from (at this point and before, our image's height should equal its initial height 'imgHeight')
const fromScroll = 0;
// final scroll position (at this point and after, our image's height should equal 'imgTargetHeight')
const toScroll = totalHeight - windowHeight;
window.addEventListener('scroll', function() {
// get current scroll position, these multiple ORs are just to account for browser inconsistencies.
let scrollPos = window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop;
// force the scroll position value used in our calculation to be between 'fromScroll` and 'toScroll'
// In this example this won't have any
// effect since fromScroll is 0 and toScroll is the final possible scroll position 'totalHeight - windowHeight',
// but they don't have to be, try setting fromScroll = 100 and toScroll = totalHeight - windowHeight - 100 for example to see the difference.
// the next line is just a shorthand for:
// if (scrollPos <= fromScroll) {
// scrollPos = fromScroll;
// } else if (scrollPos >= toScroll) {
// scrollPos = toScroll;
// } else {
// scrollPos = scrollPos;
// }
scrollPos = scrollPos <= fromScroll ? fromScroll : (scrollPos >= toScroll ? toScroll : scrollPos);
// our main calculation, how much should we add to the initial image height at our current scroll position.
const value = (imgTargetHeight - imgHeight) * (scrollPos - fromScroll) / (toScroll - fromScroll);
targetImg.style.height = imgHeight + value + "px";
});
.container {
height: 200vh;
}
.img-container {
position: fixed;
width: 100vw;
height: 100vh;
text-align: center;
background: white;
overflow: hidden;
}
.second {
background: tomato;
}
img {
position: absolute;
left: 50vw;
top: 50vh;
transform: translate(-50%, -50%);
}
<div class="container">
<div class="img-container first">
<img src="https://fixedscrollingtest-takidbrplw.now.sh/luigi.png" alt="">
</div>
<div class="img-container second">
<img src="https://fixedscrollingtest-takidbrplw.now.sh/mario.png" alt="">
</div>
</div>
I'm following a tutorial online, and this one has left me stumped after a few attempts and research.
Basically when a user scrolls down the page an image should appear when it peaks at about 50% of it's height from the bottom.
However none of my images are appearing? In the code when I loop through each image to find it's height from the bottom, and console.log it shows me it's position no problem.
const slideInAt = (window.scrollY + window.innerHeight);
When I attempt to divide the height of the image in half console.log shows me NaN.
const slideInAt = (window.scrollY + window.innerHeight) -
sliderImage.height / 2;
I'm not sure if that's preventing the images from being shown or not? Here is the full section of code I'm referring to.
const sliderImages = document.querySelectorAll('.animation-element');
function checkSlide(e) {
sliderImages.forEach(sliderImage => {
// half way through the image
const slideInAt = (window.scrollY + window.innerHeight) -
sliderImage.height / 2;
// bottom of the image
const imageBottom = sliderImage.offsetTop + sliderImage.height;
const isHalfShown = slideInAt > sliderImage.offsetTop;
const isNotScrolledPast = window.scrollY < imageBottom;
if (isHalfShown && isNotScrolledPast) {
sliderImage.classList.add('in-view');
} else {
sliderImage.classList.remove('in-view');
}
});
}
window.addEventListener('scroll', debounce(checkSlide));
The loop is also always showing false when I'm scrolling up and down in devTools.
My apologies if I'm not explaining it correctly, still learning. Here is a simplified version of my code in JSFiddle
Thanks in advance!
You are trying to get the height of a div, not an image:
function checkSlide(e) {
sliderImages.forEach(sliderImage => {
// get the height of the image, not the div
const height = sliderImage.querySelector("img").height;
const slideInAt = (window.scrollY + window.innerHeight) -
height / 2;
debugger;
// bottom of the image
const imageBottom = sliderImage.offsetTop + height;
const isHalfShown = slideInAt > sliderImage.offsetTop;
const isNotScrolledPast = window.scrollY < imageBottom;
if (isHalfShown && isNotScrolledPast) {
sliderImage.classList.add('in-view');
} else {
sliderImage.classList.remove('in-view');
}
});
}
https://jsfiddle.net/5eog6z42/2/