I am trying to achieve a certain effect with a navbar. The challenges I am facing are as below:
The navigation should appear only when the user scrolls to a certain component on the website and it should work the other way around when scrolling up
There should be a progress bar in the navigation on the scroll
I could only find how to make that starting from the top of the website but not starting from a certain point on the website
I don't need the code, just an idea of how to approach this. Maybe there are some good libraries that could help.
Thanks!
In vanilla javascript we can check the body scroll height and get it like this:
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
After you get the scrollTop value you can just make an if statement and display navbar when scroll gets to your desired value using css properties such as display, opacity or even height. You can even add the transition on these properties in css:
.navbar: {
transition: height 1s, opacity 1s;
}
For the progress bar you can just make a div inside the navbar, that scales with the scrollTop variable. You can calculate at what percentage progress bar should be like this:
addEventListener("scroll", event => {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var progressPercent = (scrollTop / height) * 100
console.log(progressPercent + "%");
});
You need event listener to update the value every time you use the scroll. In react you can do this in the useEffect() hook, but don't forget to remove it in a cleanup function, return part of useEffect() or you will end up with more event listeners every time you use the scroll and kill the page performance as the number of them increases. Read more about this in this article.
Perhaps you're looking for React's onMouseEnter and onMouseLeave events on your component to show/hide your navbar and start your progress bar
<Component onMouseEnter={() => {showNavBar()}} />
Events Documentation
Related
I have explored few approaches to this, but none really seems to work exactly like I would like:
I would like that when scrolling down, navbar is moving up at the speed the user is scrolling down, like that is static at that point.
When it disappears, I would like that the bottom of it is still visible, because this is where I have a progress bar (but maybe progress bar should detach at that point and be on top of the viewport).
When you scroll up, I would like that navbar appears again, again at the speed of scrolling, like it is static, until you see the whole navbar, when it should stick to the top of the viewport.
Here is an example of behavior I would like, but not performance/experience (because behavior is implemented using scroll event, it is not smooth).
I have also attempted to use CSS transform, which would on scroll down add a class to hide the navbar, and scroll up remove the class, animating the navbar hiding/showing, but the issue with that is that animation speed is disconnected with scrolling speed.
I tried CSS sticky position as well, but it looks like I need the opposite of what it provides.
Is there some other way to make this work well?
I've looked at your problem and I think i found a simple approach.
with this simple function you can get the amount of pixels user has scrolled.
window.onscroll = function (e) {
console.log(window.scrollY); // Value of scroll Y in px
};
after user scrolls the desired amount of pixels, make the progress bar fixed top ( or position:fixed;top:0)
Checking the link you provided, it seems to work as expected (you want it to be linked to the scroll event since you want it to move as "static"). If, though, it staggers on some system due to inconsistent scroll events, you could try adding a transition property with a small enough duration. Keep in mind the this should only be enabled while the position property remains the same, otherwise when changing from "absolute" to "fixed" it would mess things up, since the coordinate origin changes.
So you can add another variable let changedPosition = false; and whenever you change the position property you can do
if (position !== "absolute") {
changedPosition = true;
} else {
changedPosition = false;
}
position = "absolute";
or
if (position !== "fixed") {
changedPosition = true;
} else {
changedPosition = false;
}
position = "fixed";
and when you apply the style do
navbar.style = `position: ${position}; top: ${navbarTop}px; transitiona: ${
changedPosition ? "none" : "100ms linear"
}`;
like https://codepen.io/gpetrioli/pen/XWVKxNG?editors=0010
You should play around a bit with the transition properties you provide, i just put some sample values there.
I have a slider that slides from right to left as the user scrolls down the page.
I want the slider to fade out when the last slide is out of view when the user scrolls down, and I want the slider to fade in when the last slide is in view when the user scrolls up.
An example of what I want can be seen here
I have the slider working fine but am having trouble with the fade out/fade in animation.
My idea was to first store an opacity value of 1 in a useRef
const countRefOpacity = useRef(1);
And add a wheel event to my window
useEffect(() => {
window.addEventListener("wheel", handleWheel);
return () => {
window.removeEventListener("wheel", handleWheel);
};
}, []);
My handleWheel function decreases my slider opacity when the user scrolls down a certain point and increases the opacity when the user scrolls up a certain point
const handleWheel = (e) => {
if (window.scrollY >= pan.current?.offsetTop * 2 && e.deltaY > 0) {
countRefOpacity.current = countRefOpacity.current - 0.007;
slider.current.style.opacity = countRefOpacity.current;
} else if (window.scrollY < pan.current?.offsetTop * 2 && e.deltaY < 0) {
countRefOpacity.current = countRefOpacity.current + 0.007;
slider.current.style.opacity = countRefOpacity.current;
}
};
I can see the fade out animation working when the user scrolls down, but the fade in animation is pretty slow when the user scrolls up.
How can I revise my code so I can make the fade in/fade out animation smooth and natural?
Codesandbox Link
If you want a CSS property to change smoothly, you can use the transition property, e.g. in your case
transition: opacity .5s;
So, assuming I understood your question correctly, I would probably use the transition property as well as add an event listener to the window's scroll event that checks whether the user has scrolled past a certain point and updates the opacity accordingly, i.e. sets it to 1 if it's before that point and to 0 otherwise. There's no need for you to calculate a delta since the transition property will handle the fade-in/fade-out for you.
Also, I believe you probably want to use scroll instead of wheel, since I think wheel only fires when you use a mouse wheel and doesn't apply to other types of scrolling.
The idea is to change the .wrapper background depending on the current section. Unfortunately, there are some limitations. I need to change it based on the current window.scrollTop and section.offsetTop and NOT on which section is in the viewport at the moment. (IntersectionObserver will not work)
It will be something like scrollSpy, but I am not able to loop over all of the sections in every scroll event because of performance reasons. Also, I am using a virtual scroll plugin for smooth scrolling, which makes even more calculations on scroll.
To the code I have till the moment:
My sections look like this:
<section class="intro"
data-bg="#ffffff"
data-color="#1A1919"
>
Section content goes here
</section>
I have an array of objects. In each object, I have the node element and its top.
const sections = [
{
el: section.intro, // Node
top: 1224 // Number
},
....
]
I can take the scrollTop from the plugin on scroll. So the code for now looks like:
const wrapper = document.querySelector('.wrapper');
const changeBg = (scrollTop, sections) => {
sections.map(({ el, top }) => {
const bg = el.getAttribute('data-bg');
const color = el.getAttribute('data-color');
if (top < scrollTop + window.innerHeight) {
console.log(bg);
wrapper.style.backgroundColor = bg;
wrapper.style.color = color;
}
});
};
myCustomPluginForScroll.on('scroll', e => {
const { y } = e.scroll;
changeBg(y, sections);
});
The problems are:
I loop all of the sections on every scroll event (which is multiplied by many from the plugin to make smooth scrolling) and it causes lag on scroll;
As I scroll more downwards, more and more sections become valid for the condition top < y + window.innerHeight which changes the wrapper background even more times per scroll.
I can't use debouncer to make number of calls of changeBg lower, because of the smooth scroll plugin.
To the question:
How can I make call of changeBg only once when new section is added/removed from the condition of top < y + window.innerHeight and take the bg/color attributes only on the last added/removed section.
Attaching screenshot with log of bg with 6 sections and scrolled to the bottom of the site. That way, on every single scroll I have 6 changes of wrapper's background.
I’ve created an animation for my website to change a certain element (for example its background colour) while scrolling using Vanilla JS. For this I have used the window.onscroll property and I trigger the animation when window.scrollY reaches a specific position, my code is:
window.addEventListener('scroll', () => {
if (window.scrollX >= 1000) {
box.style = "background:red"
}
})
It looks great when I am editing on my big screen resolution, but if I look at the page on my laptop, the animation gets messed up because the innerWidth and innerHeight of the screen are different. I want to trigger the animation dynamically if it reaches a certain section of the page without having to worry about the scroll position.
Does anyone have any ideas about how I can fix this?
I think using getBoundingClientRect() solves your problem.
You basically do something like this:
var my_box = document.getElementById('my-box');
var rect = my_box.getBoundingClientRect();
The rect variable now contains an Object which includes a top and a left property with values that are relative to the viewport.
https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
My problem
I am making a vertical website for a client who wishes to have the window "snap" to the nearest page when most of the element is visible in the viewport. So, if the page is 85% visible, it should scroll to be 100% visible.
My problem is that occasionally when scrolling all the way to the top or bottom of the viewport, the viewport will "stick" to the first or last element, preventing a few scroll events and causing a highly noticeable flicker.
A working fiddle is here: http://jsfiddle.net/RTzu8/1/
To reproduce the error, use the scrollbar to scroll to the bottom of the page. Then, scroll up with your mousewheel. You should see the flicker. Sometimes it takes a few refreshes or attempts, but the issue is highly reproducible.
I'm at a loss as to what could be causing this issue. See below for a run-down of my code and what I have tried to prevent it so far.
My code
To accomplish my snapping, I needed to detect whether an element was a certain percentage visible. So, I added a jQuery function, isNearScreen, below. I have thoroughly tested this function, and as far as I can tell it returns accurate results.
//Modification of http://upshots.org/javascript/jquery-test-if-element-is-in-viewport-visible-on-screen
//Returns "true" if element is percent visible within the viewport
$.fn.isNearScreen = function(percent){
var offset = 1 - percent;
var win = $(window);
var viewport = {
top : win.scrollTop()
};
viewport.bottom = viewport.top + win.height();
var bounds = this.offset();
bounds.bottom = bounds.top + this.outerHeight();
bounds.top = bounds.top;
//If the element is visible
if(!(viewport.bottom < bounds.top || viewport.top > bounds.bottom)){
//Get the percentage of the element that's visible
var percentage = (viewport.bottom - bounds.top) / this.height();
//If it's greater than percent, but less than 1 + (1 - percent), return true;
return (percentage > (1 - offset) && percentage < (1 + offset));
}
return false;
};
I then created a snap function, which makes use of Underscore.js's _.debounce function, to only fire on the trailing end of continuous scroll events. It fires after a 500ms timeout, and I am fairly (though not 100%) convinced that it is firing correctly. I have not been able to reproduce console logs that would indicate multiple concurrent firings.
//Snaps the screen to a page after scroll input has stopped arriving.
var snap = _.debounce(function(event){
//Check each page view
$.each($('.page-contents'), function(index, element){
//If the page view is 70% of the screen and we are allowed to snap, snap into view
if($(element).isNearScreen(0.7)){
$('html,body').animate({
scrollTop: $(element).offset().top
}, 300);
}
});
}, 500);
Finally, I bind to the window's scroll event
$(window).on('scroll', snap});
The (extremely simplified) HTML:
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
<div class="page">
<div class="page-contents"></div>
</div>
and CSS:
.page{
height: 750px;
width: 100%;
margin: 10px 0;
background: gray;
}
.page-contents{
height: 100%;
width: 100%;
}
What I've tried
I have tried the following, with no success:
Setting a boolean, 'preventSnap', on the window, checking its state, and only firing the animate portion of snap if it is set to false. After animation, set it to true, then set it to false after 500ms (which should in theory prevent double firings).
Calling .stop() on the element before running the snap animation.
Calling event.preventDefault() on the scroll event before running the animation.
Reducing and increasing my _.debounce delay. Interestingly, a lower _.debounce delay (200-300ms) seems to aggravate the problem and a higher _.debounce delay (1000ms) seems to fix it. This is not an acceptable solution, however, as it feels "long" waiting 1sec for the page to "snap".
Changing the heights of the elements
If there is any other information I can provide, please let me know. I'm at a loss!
I think this is a combination of events and how _.debounce works. I noticed in the fiddle (in Chrome) that the elements were 'jitterring' long after the snap finished. If you put a console log in the snap event handler you can see it's constantly being called after a snap even with no scroll inputs.
This must be the scroll animation itself setting off the snap, I tried to set a flag to prevent dual snapping and clearing the flag after the animation was finished -- however that didn't work I think because _.debounce is queuing the event to happen later (after the animation finishes and clears the flag).
So what does work is to add this as the start of the snap handler:
var nosnap = false;
var snap = _.debounce(function(event){
// Don't snap if already animating ...
if (nosnap) { nosnap = false; return; }
nosnap = true;
Fiddle
That prevents the animation directly firing the next snap event -- however that's going to cause issues if you scroll again during the animation.
So, that's a bit of a hack. Ideally you want to be able to tell what's causing the scroll event and react accordingly but there's no easy way to do that.
I absolutely think you need to stop the animation when handling a second scroll event as well.