CSS scroll-snap-type with Horizontal Scrolling

3k views Asked by At

I made a website that makes use of Bootstrap 4, horizontal scrolling (via jquery.mousewheel.js) and native CSS Snapping. I am using Chrome 81, and don't care about old/unsupported browsers.

When I apply scroll-snap-type: x mandatory; to CSS, the horizontal scrolling stops working (no scrolling happens at all). If the CSS property is removed, the scrolling behaves normally. Here is my HTML structure:

<div class="portfolio_content container-fluid h-100">
  <div class="row h-100 flex-row flex-nowrap scrollx long_content">
    <div class="col-12 h-100 project d-table" id="project1">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 1
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project2">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 2
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project3">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 3
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project4">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 4
        </p>
      </div>
    </div>
  </div>
</div>

For CSS, the key entries are here:

html, body {
    height: 100%;
}
.portfolio_content .scrollx {
    overflow-x: scroll;
}

.portfolio_content .long_content {
    scroll-snap-type: x mandatory; /* Comment this to make scrolling works */
}
.portfolio_content .project {
    scroll-snap-align: start;
}
.portfolio_content .project_title {
    display: block;
    margin: auto;
    text-align: center;
}
.portfolio_content .project_title h1 {
    font-size: 96px;
    line-height: 0.8;
}

Last, here is the JSFiddle demo. If you comment line 9 in CSS, the demo will be able to scroll.

My aim is to allow the "project" DIV to be snapped when scrolling, i.e. one "project" DIV will be fully shown after scrolling.

How should I change the codes?

3

There are 3 answers

3
vqf On

Short answer: take away the javascript code.

Long answer: mousewheel.js is useful if you want to check or do something on wheel events. In this case, you simply want to let the browser scroll. You are taking control of that event in the function and preventing the default, but the default action is exactly what you want.

When you execute this.scrollLeft -= delta;, it does two things: scroll and then end scroll. the scroll-snap-type listens to end scroll and snaps the element. Actually, if you take away only the e.preventDefault(); line, a funny thing happens. The element vibrates and then changes. In my understanding, what is happening is: 1) Default scroll event, which takes some time. Meanwhile, 2) wheel event, which triggers a much shorter scroll event. 3) snap. Since the deltaX from 2 is small, you get to the starting position. repeat 2 and 3 several times. 4) Default scroll event ends with a much larger deltaX. 5) snap.

In summary, for the behavior you want, it is better to let the browser do the scrolling. If you want to do something else, you need to avoid sending an end scroll signal every time a mousewheel event fires. To do that, you can simply eliminate the two lines of code inside the mousewheel function and add the code you need.

EDIT after comment:

I should have guessed by the context, you want to transform vertical wheel events into horizontal wheel events. The former answer works if you send a horizontal wheel event (you can do that with a touchpad). The long answer still stands, so the question is how to bypass the problem. I thought of a couple of strategies:

  • Cancel the event and create a synthetic WheelEvent. By far the most elegant solution, but I could not make it work. The event gets dispatched but no scrolling occurs:
 let lc = document.querySelector('.long_content');
 lc.addEventListener('wheel', function(e) {
   if (e.deltaY !== 0){
     e.preventDefault();
     let evt = new WheelEvent('wheel', {
       deltaX: e.deltaY
     });
     this.dispatchEvent(evt);
   }
   else{
     // The new event gets captured here
   }
  });

EDIT 2

There are many strategies to make this work, but all of them have their disadvantages. The previous one had minimal impact on your script, but the change started after the wheel event ended. This and other parameters are left to the browser, and will not be under your control.

I have added a second fiddle with another strategy that gives the behavior you seem to want (https://jsfiddle.net/vhywmdb5/). I hope it helps, but every solution needs to be tweaked and there are no warranties that it will work with every browser. Unfortunately, the event simulation in mousewheel.js is broken at this time.

4
SilverSurfer On

You dont need an external plugin when you can do it with a simple script.

$(".portfolio_content .project").on('wheel', function(e) {
    e.preventDefault();        
    if (e.originalEvent.wheelDelta >= 0) {
        if ($(this).prev().length) {
            var prev = "#" + $(this).prev().attr("id");
            document.querySelector(prev).scrollIntoView({behavior: 'smooth'});
        }
    } else {
        if ($(this).next().length) {
            var next = "#" + $(this).next().attr("id");
            document.querySelector(next).scrollIntoView({behavior: 'smooth'});
        }
    }
});
html, body {
    height: 100%;
}
.portfolio_content .scrollx {
    overflow-x: scroll;
}

.portfolio_content .project {
 scroll-snap-align: start;
}
.portfolio_content .project_title {
 display: block;
 margin: auto;
 text-align: center;
}
.portfolio_content .project_title h1 {
 font-size: 96px;
 line-height: 0.8;
}
#project1, #project3 {
    background-color: #EEE;
}
#project2, #project4 {
    background-color: #CCC;
}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

<div class="portfolio_content container-fluid h-100">
  <div class="row h-100 flex-row flex-nowrap scrollx long_content">
    <div class="col-12 h-100 project d-table" id="project1">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 1
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project2">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 2
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project3">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 3
        </p>
      </div>
    </div>
    <div class="col-12 h-100 project d-table" id="project4">
      <div class="project_title d-table-cell align-middle">
        <p>
          Project 4
        </p>
      </div>
    </div>
  </div>
</div>

0
Plippie On

You need to convert the mousewheel delta from X to Y and apply it to the element which has the scrollbar, also prevent default on the vertical mousewheel interaction of the container, but you need it to set it to the width of the container or it won't be enough to push.

 // 'swipe' left or right with mouse wheel:
const container = document.querySelector('.long_content');

container.addEventListener('wheel', (evt) => {

evt.preventDefault(); 

//scroll deirection
 let delta = evt.deltaY;
 //take widht of container:
let contWidth = evt.target.offsetWidth;

// check direction and apply to contWidth
if(delta < 0){
contWidth = -contWidth;
}
     
  container.scrollLeft += contWidth;  

});

Here is the updated fiddle