I have a problem with CSS scroll snap. I want to detect the snapped element via JavaScript and assign it, e.g., a CSS class or similar.
Unfortunately, I haven't found a way to detect the snapped element yet. Background: I have a list with subitems, which are scrolled, always the middle item in the list should be highlighted:
I already tested the intersection observer with rootMargin to detect the vertically centered element, but it’s more buggy than useful.
<div class="timeline-menu-dropdown-years-list-container">
<ul class="timeline-menu-dropdown-years-list timeline-menu-dropdown-years-text" id="yearcontainer">
<li id="2010" class="timeline-dropdown-year" data-target="year-2010">2010</li>
<li id="2009" class="timeline-dropdown-year" data-target="year-2009">2009</li>
<li id="2008" class="timeline-dropdown-year" data-target="year-2008">2008</li>
<li id="2007" class="timeline-dropdown-year" data-target="year-2007">2007</li>
<li id="2006" class="timeline-dropdown-year" data-target="year-2006">2006</li>
<li id="2005" class="timeline-dropdown-year" data-target="year-2005">2005</li>
<li id="2004" class="timeline-dropdown-year" data-target="year-2004">2004</li>
<li id="2003" class="timeline-dropdown-year" data-target="year-2003">2003</li>
<li id="2002" class="timeline-dropdown-year" data-target="year-2002">2002</li>
<li id="2001" class="timeline-dropdown-year" data-target="year-2001">2001</li>
<li id="2000" class="timeline-dropdown-year" data-target="year-2000">2000</li>
</ul>
</div>
.timeline-menu-dropdown-years-list-container {
max-height: 250px;
overflow: scroll;
scroll-snap-type: y mandatory;
-ms-overflow-style: none; /* Internet Explorer and Edge */
scrollbar-width: none; /* Firefox */
padding-top: 45%;
padding-bottom: 40%;
scroll-padding-top: 45%;
scroll-padding-bottom: 40%;
}
.timeline-dropdown-year {
color: white;
font-size: 14px;
border-bottom: 2px solid white;
margin-right: 11%;
margin-left: 34%;
scroll-snap-align: center;
}
How can I fix it?
At the end, you should be able to scroll through this timeline. The active element should always snap to the center and be visually highlighted.
An improvement over the current answer would be to store the bounds of each elements beforehand to prevent a reflow each time the scroll event is triggered.
let container = document.querySelector('.timeline-menu-dropdown-years-list-container')
let containerBounds = null
let currentItem = 0
// Store items as an array of objects
const items = Array.from(document.querySelectorAll('.timeline-dropdown-year')).map(el => ({el}))
const storeBounds = () =>{
// Store the bounds of the container
containerBounds = container.getBoundingClientRect() // triggers reflow
// Store the bounds of each item
items.forEach((item, i)=>{
item.bounds = item.el.getBoundingClientRect() // triggers reflow
item.offsetY = item.bounds.top - containerBounds.top // store item offset distance from container
})
}
storeBounds() // Store bounds on load
const detectCurrent = () => {
const scrollY = container.scrollTop // Container scroll position
const goal = container.bounds.height / 2 // Where we want the current item to be, 0 = top of the container
// Find item closest to the goal
currentItem = items.reduce((prev, curr) => {
return (Math.abs(curr.offsetY - scrollY - goal) < Math.abs(prev.offsetY - scrollY - goal) ? curr : prev); // return the closest to the goal
});
// Do stuff with currentItem
// here
}
detectCurrent() // Detect the current item on load
window.addEventListener('scroll', () => detectCurrent()) // Detect current item on scroll
window.addEventListener('resize', () => storeBounds()) // Update bounds on resize in case they have changed
A further improvement would to have a debounce
function on the scroll event so as to reduce how often the calculation is done. We do not really need to check every time the scroll event is fired and could set an interval of around 200 ms between each check.
You may also need to update the bounds when the page is resized depending if the layout changes or not.
Resources for those not familiar with how browser reflow works:
I have the same problem. I solved it with JavaScript here: Implementation of CSS scroll snap event stop and element position detection
[].slice.call(container.children).forEach(function (ele, index) {
if (Math.abs(ele.getBoundingClientRect().left - container.getBoundingClientRect().left) < 10) {
// The 'ele' element at this moment is the element currently
// positioned. Add class .active for example!
} else {
// The 'ele' element at the moment is not
// the currently positioned element
}
});
Put this in the event scroll:
// Timer, used to detect whether horizontal scrolling is over
var timer = null;
// Scrolling event start
container.addEventListener('scroll', function () {
clearTimeout(timer);
// Renew timer
timer = setTimeout(function () {
// No scrolling event triggered. It is considered that
// scrolling has stopped do what you want to do, such
// as callback processing
}, 100);
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With