Scroll Snap only when scrolling down
Asked Answered
C

4

8

I am struggling with scroll-snap in CSS.

Is there a way to use scroll-snap only in one vertical direction?

I want to make it snap on every section when scrolling down, but not to snap when scrolling back up again. Is it possible inside CSS or do I need some JavaScript?

html {
    scroll-snap-type: y mandatory;
}

body {
    margin: 0;
    scroll-snap-type: y mandatory;
  }
  
  section {
    font-family: sans-serif;
    font-size: 12ch;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    scroll-snap-align: start; 
  }
 
  <head>

    <!-- MAIN STYLE -->
    <link rel="stylesheet" href="style.css">
    

  </head>

    <main>
     <section>
        <h1>Hello!</h1>
     </section>

     <section>
        <h1>Toodles~</h1>
     </section>

     <section>
        <h1>Boodles~</h1>
     </section>
            
    </main>

</html> 
Calpe answered 10/12, 2021 at 11:53 Comment(1)
To answer the main question: you need JS. – Materiel
D
4

Unfortunately, you'll need JavaScript to change scrolling behavior when the scrolling direction changes.

🀷 I'm Not 100% Sure Why It Works...

I immediately thought that:

  • registering the "scroll" event to window
  • assigning scroll-snap-type: y mandatory to <body>✣
  • and getting a distance from window.scrollYπŸ—‘ to compare with.

This and many other combinations derived from here and here eluded me. Upon further digging, the CSS property, scroll-behavior will not propigate from the <body> to the viewport (aka window) -- instead it's the <html> tag that'll actually be detected when a "scroll" event is triggered. Although scroll-behavior is for scrolling from links, it's the only clue that lead me in the right direction, and I can't find a real good explination as to why it appears that CSS scrolling depends solely on the root. I believe that since the content takes up the whole viewport, window is the best choice to bind the event handler to and that CSS scrolling doesn't bubble the standard way.

✣<html>
πŸ—‘document.body.getBoundingClientRect().top


Details are commented in exaple below

Note: The iframe in which Stack Snippets reside within are not at all a close proximity to what the real dimaensions are. To get the true dimensions (like viewport) and associated behavior (like scrolling), review it in Full page

                                 fullpage

<!DOCTYPE html>
<html lang="en">

<head>
  <title></title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <style>
     :root {
      font-family: 'Georgia';
      font-size: 62.5%;
      line-height: 1;
      scroll-behavior: smooth;
    }
    
    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    }
    
    section {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      font-size: 1.6rem;
      outline: 5px dashed tomato;
      scroll-snap-align: start;
    }
    
    section:nth-of-type(odd) {
      color: gold;
      background: navy;
    }
    
    section:nth-of-type(even) {
      color: lime;
      background: black;
    }
    
    h1 {
      font-size: 6em;
      margin: 0;
    }
  </style>
</head>

<body>
  <section><h1>I</h1></section>
  <section><h1>II</h1></section>
  <section><h1>III</h1></section>
  <section><h1>IV</h1></section>
  <section><h1>V</h1></section>
  <section><h1>VI</h1></section>
  <section><h1>VII</h1></section>
  <section><h1>VIII</h1></section>
  <section><h1>IX</h1></section>
  <section><h1>X</h1></section>

  <script>
/*
Declare an initial value outside of event handler so that it is retained
after the function is done. 
*/
 let start = 0;
 // Bind window to the scroll event
 window.addEventListener('scroll', function() {
   // Reference the BCR of <body>
   const bcr = document.body.getBoundingClientRect();
   /*
   Get the distance from the top of viewport and the top of the body
   */
   const stop = bcr.top;
   /*
   If that distance greater than start, disable scroll-snap-type on <html>
   */
   if (stop > start) {
     document.scrollingElement.style.setProperty('scroll-snap-type', 'none');
   //console.log('up')
   } else {
   // Otherwise, enable scroll-snap-type
     document.scrollingElement.style.setProperty('scroll-snap-type', 'y mandatory');
   //console.log('down');
   }
   // Set start as stop for the next scroll
   start = stop;
 });
 </script>
</body>

</html>
Devonian answered 4/4, 2022 at 9:33 Comment(0)
C
3

You could listen the the scroll event and set the scroll-snap-type property to the html element according to the scroll direction.

//save initial scrollY position
var previousScrollY = window.scrollY;
//listen to scroll event
window.addEventListener("scroll", function(e) {
  //is the user scrolling down ?
  let down = window.scrollY > previousScrollY;
  //set scroll-snap-type according to scroll direction
  document.documentElement.style.scrollSnapType = down ? "y mandatory" : "";
  //save current scrollY position
  previousScrollY = window.scrollY;
});
body {
  margin: 0;
}
  
section {
  font-family: sans-serif;
  font-size: 12ch;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  scroll-snap-align: start; 
}
<section>
    <h1>Hello!</h1>
 </section>

 <section>
    <h1>Toodles~</h1>
 </section>

 <section>
    <h1>Boodles~</h1>
 </section>
Cragsman answered 29/3, 2022 at 11:51 Comment(1)
Nice example, but if you play long enough with your demo (in full-page, not to talk when the document height is small like in the snippet) is seems like a pretty broken UI, leading to a poor UX. – Chrysanthemum
P
1

As per my research, no solution using CSS, and to achieve this use a javascript window scroll event and on scroll up remove the scrollSnapType CSS property.

Pewee answered 31/3, 2022 at 15:19 Comment(0)
P
1

Yes, You need a little bit of javascript to achieve the functionality you want. This is an elegant solutions to the problem:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Scroll Snap</title>
    <style>
        *, ::after, ::before {
            margin: 0;
            box-sizing: border-box;
        }
        body {
            height: 100vh;
            overflow: hidden;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
            scroll-behavior: smooth;
        }
        .container {
            height: 100vh;
            overflow: hidden scroll;
        }
        .container.snap-scroll {
            scroll-snap-type: y mandatory;
        }
        section {
            height: 100vh;
            font-size: 3rem;
            display: flex;
            justify-content: center;
            align-items: center;
            scroll-snap-align: start;
        }
        section {
            font-weight: bold;
        }
        section span {
            background-color: #80808020;
            margin-left: 1rem;
            padding: .25rem 1rem;
            border-radius: .25rem;
            color: #161616;
        }
        .bg-color-bisque {
            background-color: bisque;
        }
        .bg-color-white {
            background-color: white;
        }
        .bg-color-burlywood {
            background-color: rgb(25, 226, 209);
        }
    </style>
</head>
<body>
    <div class="container snap-scroll">
        <section class="bg-color-bisque">Section <span>1</span></section>
        <section class="bg-color-white">Section <span>2</span></section>
        <section class="bg-color-burlywood">Section <span>3</span></section>
    </div>
    <script>
        let container = document.querySelector('.container');
        let lastScrollTop = 0;
        
        container.addEventListener('scroll', checkScrollDirection)

        function checkScrollDirection(event) {
            let currentPosition = container.scrollTop;
            if (currentPosition > lastScrollTop){
                // On scroll down
                container.classList.add('snap-scroll')
            } else {
                // On scroll up
                container.classList.remove('snap-scroll')
            }
            lastScrollTop = currentPosition <= 0 ? 0 : currentPosition;
        }
    </script>
</body>
</html>
Pediment answered 2/4, 2022 at 13:48 Comment(2)
This is on wheel event, not scroll? – Objectionable
Yeah, you're right. Let me refactor. – Pediment

© 2022 - 2024 β€” McMap. All rights reserved.