admin管理员组

文章数量:1291294

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

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.

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

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.

Share Improve this question edited May 7, 2022 at 19:33 Peter Mortensen 31.6k22 gold badges110 silver badges133 bronze badges asked Mar 29, 2021 at 9:56 Fabio ThomaFabio Thoma 1181 gold badge1 silver badge6 bronze badges 9
  • I can not reproduce your code. Maybe you find a specific class or id of the current date in your list in Developer Tools to detect the snapped element. Cause of in your Screenshot I can see that the snapped element has different css styling. So it should have something unique. – Bernhard Beatus Commented Mar 29, 2021 at 10:27
  • 1 The code is just the base. The li elements snap in, i would like to detect the current snapped item and assign a css class to it (e. g. "active") – Fabio Thoma Commented Mar 29, 2021 at 12:03
  • I examine the dom when scrolling. As I see there is no dom change when its scrolling. – Bernhard Beatus Commented Mar 29, 2021 at 13:05
  • 1 I know I'm looking for a way to find out when an item is snapped so I can manipulate it afterwards. – Fabio Thoma Commented Mar 30, 2021 at 11:35
  • Possibly related: CSS Scroll snap not snapping on to sections – Peter Mortensen Commented May 7, 2022 at 19:31
 |  Show 4 more ments

2 Answers 2

Reset to default 3

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);
});

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

本文标签: javascriptCSS Scroll Snap get active ItemStack Overflow