Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS Scroll Snap get active Item

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:

Layout

Layout

I already tested the intersection observer with rootMargin to detect the vertically centered element, but it’s more buggy than useful.

HTML

<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>

CSS

.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.

like image 726
Fabio Thoma Avatar asked Nov 07 '22 02:11

Fabio Thoma


2 Answers

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:

  • Minimizing browser reflow
  • What forces layout / reflow
like image 38
The Sloth Avatar answered Nov 15 '22 07:11

The Sloth


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);
});
like image 50
Matrix Avatar answered Nov 15 '22 05:11

Matrix