bootstrap
bootstrap copied to clipboard
ScrollSpy not behaving correctly
Scroll spy on body element will only activate the first item on the nav menu and will not update accordingly, Boostrap 5.1.3 works fine but for some reason 5.2 beta doesn't
Scrollspy changed completely in 5.2. I had the same problem and figured out that Scrollspy must be added to the wrapping element of the sections. This is usually NOT the <body>.
<body>
<main>
<div class="wrapper"> <!-- Add Scrollspy here -->
<section id="one">
...
</section>
<section id="two">
...
</section>
<section id="three">
...
</section>
</div>
</main>
</body>
Bug reports must include a live demo of the issue. Per our contributing guidelines, please create a reduced test case via CodePen or JS Bin and report back with your link, Bootstrap version, and specific browser and operating system details.
This is a saved reply.
Hello @omar-abdul. Bug reports must include a live demo of the issue. Per our contributing guidelines, please create a reduced test case on CodePen or JS Bin and report back with your link, Bootstrap version, and specific browser and Operating System details.
Scrollspy has clearly regressed, even on the docs. The prior behavior was to make a link active if its associated element reached the scroll top. Now, it seems to move the active state if one element is more visible than the other. This does not get the same results, and has very strange behavior in various different scenarios which expected the previous behavior. This is just the coded behavior, it's not browser dependent.
Here is an example of the difference on the docs page itself.
https://user-images.githubusercontent.com/2672245/170849218-7faab47f-67e6-4295-8265-8544725b4d35.mp4
Here's one example of how it broke. This is just me making the paragraph longer on the 5.2 docs.
https://user-images.githubusercontent.com/2672245/170849451-0552fe36-70dd-4311-96e9-8de918a9631f.mp4
There's just no active link, since the heading is not visible. There is absolutely no way to handle this correctly with the current implementation, since if you have a wrapper element around each heading and paragraph, then it does not activate properly at all (since the large wrapper element will not satisfy the thresholds properly). And you can fiddle around with the root margin and maybe thresholds to help fit the content within the intersection parameters, but it will be specific to each section, so there is no setup that works with varying bits of spied content sections.
I have the same issue with version 5.2 regarding the ScrollSpy skipping to the newly observed element when scrolling. This does not provide a good user experience and will confuse the user as they are scrolling.
In this video, I updated the height from 200px to 600px which shows the same issue I'm having when using the ScrollSpy.
https://user-images.githubusercontent.com/62732616/173135284-76a83643-1b23-4448-b2c4-1eadb4db0d20.mov
POC: IntersectionObserver observing the element that is being scrolled into the viewport which mimics the ScrollSpy buggy behavior. https://codepen.io/byondsick12/pen/oNEdqzW
POC: This is my current solution... https://codepen.io/byondsick12/pen/wvyNqqd?editors=0111
As the issue was labeled with awaiting-reply, but there has been no response in 14 days, this issue will be closed. If you have any questions, you can comment/reply.
Reopened it since a CodePen has been provided by @tranqt1984 in the meantime (haven't checked its content yet)
I noticed this issue as well. I have little experience with JS, but from what I read, it seems like with the smallest threshold at 0.1, that means that 10% of an element has to be inside the observer rectangle before the callback is called. That means if an element is more than 10x the height of the observer rectangle, this will never occur, which is why making the paragraph longer broke it.
Regarding the other issue with increasing the height of the spied element causing the spy to jump from 1 to 4 and then from 5 to 2, the observed rectangle is big enough that multiple elements are already in the observer rectangle, so the next one to enter it is not always the one immediately after the active element.
To solve this, I made the threshold 0 so that when any pixel enters the observer rectangle, the callback is called. To avoid multiple elements being inside the observer rectangle, I made the observer rectangle quite small and made the content elements have a minimum height. I placed the observer rectangle just below the top of the spied element. Whatever element is in this area should be highlighted. To do that, I set the root margin to -10% 0px -70%. I also set the minimum height of the content elements to half of the spied element. Depending on use case, I'm sure there are other techniques to accomplish the same.
Anyway, my hacky solution (again, I'm not a frontend programmer) is:
spy-container {
min-height: calc(100vh / 2.0);
}
<main ... data-bs-root-margin="-10% 0px -70%">
<div class="spy-container"></div>
<div class="spy-container"></div>
...
ss = bootstrap.ScrollSpy.getInstance("main");
ss._getNewObserver = () => {
const options = {
root: ss._rootElement,
threshold: [0],
rootMargin: ss._getRootMargin()
};
return new IntersectionObserver(entries => ss._observerCallback(entries), options);
}
ss._observer = null;
ss.refresh();
@kenrobbins #36750 this may help you to use threshold properly
There are additional pieces that are not working here. They are visible directly on the demo site. For example, navigate to...
https://getbootstrap.com/docs/5.0/components/scrollspy/#item-1-2
Scroll to "Example with nested nav"
You will notice that the scrollspy navbar is now off by two elements.
"Item 1" in the navbar is now linked to "Item 1-2"

If you scroll up to the "Item 1", nothing at all is highlighted.

And if you scroll down, the offsets continue.

Posting in case it helps anyone else -- As per the original post about Scrollspy not working on the 'body' element in 5.2. I was going through the steps to make a code-pen and found through that exercise it worked just fine. My issue was a conflict I had in the 'overflow' attribute on the 'body'. Once I cleared that up, it worked as expected.
My issue was a conflict I had in the 'overflow' attribute on the 'body'. Once I cleared that up, it worked as expected.
@qofe Can you elaborate on this?
Is there any update on this? Scrollspy and the Docs are awful now. Before Scrollspy when spying on a list of links, would highlight when the target reached the top of the viewport, but now it highlights just when the target is visible in the window anywhere. There's absolutely no guidance on how to tweak these root margins and thresholds to at least recreate the original scroll spy behavior from before 5.2 where it highlights when the target is at the top of the viewport.
I am experiencing the same issue. If a section is too short to consume enough of the vieweport, the nav item is never actually navigating.
I'd prefer that whatever is at the top of the screen is what is deemed "active".
same issue i have in react when moving downwards it working but moving upward it is not working
Same issue. ScrollSpy is not working on
@shikkaba I experienced the same as @qofe. The issue, described in the very first post, is having something like this:
body{
overflow-x: hidden; /* or auto or scroll */
}
and it was resolved by doing something like this:
body{
overflow-x: unset; /* or initial */
}
I don't say that's generally the case for this not working, but overflow is likely the issue.
I am actually working on a custom scroll-spy for a company. I was trying to get inspiration by checking the bootstrap implementation, however I found out that the IntersectionObserver only returns entries that changed in visibility. However it can be that there are 10 entries visible at load. These entries are processed, and the first entry is visible (almost always during load). When you scroll down, only entries that change in visibility are available so 11, then 12, then 13 (with entry.isIntersecting true). And likewise elements 1, then 2, then 3 (with entry.isIntersecting false).
On page load
|--------------------------------------------------|
| item 1 |
| item 2 |
| item 3 |
| item 4 |
|--------------------------------------------------|
item 5
item 6
new IntersectionObserver((entries) => {
console.log(entries);
});
// Result: [
// { id: 1, isIntersecting: true, ... },
// { id: 2, isIntersecting: true, ... },
// { id: 3, isIntersecting: true, ... },
// { id: 4, isIntersecting: true, ... },
// { id: 5, isIntersecting: false, ... },
// { id: 6, isIntersecting: false, ... },
// ]
When scrolling to where item 6 is visible and item 1 isn't will result in this
item 1
|--------------------------------------------------|
| item 2 |
| item 3 |
| item 4 |
| item 5 |
|--------------------------------------------------|
item 6
new IntersectionObserver((entries) => {
console.log(entries);
});
// Result:
// Result: [
// { id: 1, isIntersecting: false, ... },
// { id: 6, isIntersecting: true, ... },
// ]
This will trigger item 6 to be active where actually item 2 is the first visible item.
This means that solely depending on the IntersectionObserver is not possible. Also when there is nog change in visible elements it can be possible that another scroll-spy target should be made active.
A possible correct implementation would be to combine the IntersectionObserver with a onscroll where the IntersectionObserver is used to save all visible elements in some array/object and the onscroll callback uses these visible elements to determine which one is the first visible (of course only intersecting elements can be active), I expect this first option will reduce the load of the onscroll callback. Or only using a onscroll (basically going back to version 5.1).
Hope this can be of any help. I could share the vue composable implementation when I'm done however this will have to be adjusted for the needs of bootstrap (just inspiration).
My issue was a conflict I had in the 'overflow' attribute on the 'body'. Once I cleared that up, it worked as expected.
@qofe Can you elaborate on this?
Yes, I had `body {overflow-x:hidden; } in top of my styles. Removed overflow setting, changed nothing else, and that eliminated the scrollspy issue described in the first post.
I have been getting better results when wrapping the entire section that I want to spy on with the id target.
For example, instead of doing this (example given in docs):
<h4 id="scrollspyHeading1">First heading</h4>
<p>...</p>
I will do this:
<div id="scrollspyHeading1">
<h4>First heading</h4>
<p>...</p>
</div>
This way the id target is in the viewport for most of the time. This does seem to mess things up a bit when I have one section that is smaller than another that is also within the viewport.
My implementation is very similar to the old Bootstrap 3 docs with nested nav items that appear when the parent has the active state (like the sidebar here https://getbootstrap.com/docs/3.4/components/) making it very important to have the parent being spied on at all time.
I played around with the rootMargin property as well, similar to what was done in the docs, bringing it to rootMargin: '0px 0px -35%' in my case which seemed to help.
Everything seems to be working when scrolling down, but when reaching the bottom of the page and scrolling back up, there are a lot of issues. The active item spied, when clicking on a menu link, after getting to the bottom of the page is inaccurate and sometimes no active item is picked up. Some items don't receive the active state at all, especially when scrolling back up the page versus clicking on a menu link. I am thinking that this has to do with the last scrollspy target section being very short in height, so it messes with the positioning when going back up.
Just some observations and suggestions in case they help progress this issue further!
I have been getting better results when wrapping the entire section that I want to spy on with the
idtarget.
Yeah, I wrap the content with a section that references each scroll spy target and it doesn't help. The "Home" entry isn't working either. This has been broken for a while now.
So this issue just being tossed aside?
So this issue just being tossed aside?
It's not being tossed aside. We're focusing right now on the v5.3.0 release. Then we'll try to define and prioritize what comes next. I'll keep in mind this one. Please understand that there's a huge amount of work to do in Bootstrap, and everybody does it in their spare time.
In my use case, ScrollSpy does not work with sections with large height by default, and l noticed that the default threshold [0.1, 0.5, 1], typically 0.1, is not good for large height section . For me, data-bs-threshold="0,1" data-bs-root-margin="-30% 0% -70%" fixed the issue. (EDIT: Adding display: flow-root to the sections is also important to disable gaps between sections caused by child margins.)
Is there any update on this? Scrollspy and the Docs are awful now. Before Scrollspy when spying on a list of links, would highlight when the target reached the top of the viewport, but now it highlights just when the target is visible in the window anywhere. There's absolutely no guidance on how to tweak these root margins and thresholds to at least recreate the original scroll spy behavior from before 5.2 where it highlights when the target is at the top of the viewport.
I completely agree and have the same issue. Following the topic...
So this issue just being tossed aside?
It's not being tossed aside. We're focusing right now on the v5.3.0 release. Then we'll try to define and prioritize what comes next. I'll keep in mind this one. Please understand that there's a huge amount of work to do in Bootstrap, and everybody does it in their spare time.
All I meant was, this Issue is no longer assigned to any project, so I see this falling through the cracks. It's already been an open issue and a problem for 15 months.
Another example that scrollspy is not working in Bootstrap 5.3.2 Windows 10 64-bit Firefox 112.0, (MS Edge 120.0.2210.144) https://jsfiddle.net/2oxrkzyb/
The "Two" options doesn't get activated.
If you swap resources to 5.1.3 it's working fine.
What I figured is that the ScrollSpy seems to work when wrapping the seciont and making clear sections with that.
However, especially in responsive view (iPhone - roughly 400px) - it seems that very large sections are again not properly tracked by ScrollSpy. I am just checking out if it is an issue with the zIndex but that'd surprise me as of now.
For example my whole page has a total height of 16630 px, and the largest section clocks in with 7350 pixels. I'd add screenshots but its not giving much information in responsive view - one time the active class is added to a nav, one time it isn't.
Has anyone managed to fix this? Even on the documentation page for v5.3 it's not working as it should.