CSS Partial Scroll Snapping

Viewed 817

Been playing around with the scroll snapping, looks like it saves a lot of head scratching with writing the functionality in JS.

Here's a challenge, has anyone out there found a way to selectively choose which children to snap and which children to freely scroll?

I think this will be useful for content rich pages that contain parts that wouldn't benefit from scroll snapping.

Here's an example of the problem: https://codepen.io/nodelondon/pen/YzxWqLG

html {
  background: #f2f2f2;
}

.scroll-container,
.scroll-area-none,
.scroll-area {
  max-width: 850px;
  height: 600px;
  font-size: 60px;
}

.scroll-area-none {
  scroll-snap-align: none;
  background-color: black;
}

.scroll-container {
  overflow: auto;
  scroll-snap-type: y mandatory;
}

.scroll-area {
  scroll-snap-align: start;
}

.scroll-container,
.scroll-area-none,
.scroll-area {
  margin: 0 auto;
}

.scroll-area-none,
.scroll-area {
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
}

.scroll-area:nth-of-type(4n+1) {
  background: #49b293;
}

.scroll-area:nth-of-type(4n+2) {
  background: #c94e4b;
}

.scroll-area:nth-of-type(4n+3) {
  background: #4cc1be;
}

.scroll-area:nth-of-type(4n+4) {
  background: #8360A6;
}
<div class="support-scrollsnap"></div>

<div class="scroll-container">
  <div class="scroll-area-none">-1</div>
  <div class="scroll-area-none">0</div>
  <div class="scroll-area">1</div>
  <div class="scroll-area">2</div>
  <div class="scroll-area">3</div>
  <div class="scroll-area">4</div>
  <div class="scroll-area-none">5</div>
  <div class="scroll-area-none">6</div>
</div>

Ideally, the boxes with -1,0,5 and 6 should be able to freely scroll but the mandatory boxes in between keep pulling you back.

If you're thinking of suggesting changing it to proximity, this is a good suggestion, but, with IOS Safari (On OSX Safari for me as well), unfortunately it still forces scroll snapping on boxes that have scroll-snap-align set to Start no matter where you are on the page.

2 Answers

Found that changing scroll-snap-type on documentElement causes the scroll to jump to the nearest snap-align element, which seems to look awful.

Looks like the simplest fix is working fine:

let t = window.scrollY;
requestAnimationFrame(() => window.scroll(0, t));

I propose the following logic:

  1. Define which children block is at the top border of the scroll container. I am using the Element.getBoundingClientRect() method to compare positions of the scroll container and its children.
  2. Check which scroll-snap-align property has this child block.
  3. Set the scroll-snap-type property of the container as y proximity or y mandatory.
  4. Handle the scroll event on desktop, On mobile this event works at the end of the scroll, so we need something else for mobile (may be jQuery Mobile).

Here is a working draft solution. But it requires optimizations and improvements such as scroll event throttling.

https://codepen.io/glebkema/pen/zYdPqeY

let scrollContainers = document.getElementsByClassName('scroll-container');
for (let sc of scrollContainers) {
  sc.addEventListener('scroll', updateSnapType);
  sc.addEventListener('touchstart', updateSnapType);
}

function updateSnapType(event) {
  let parent = event.currentTarget;
  let parentRect = parent.getBoundingClientRect();
  for (let child of parent.children) {
    let childRect = child.getBoundingClientRect();
    if (childRect.top <= parentRect.top && parentRect.top < childRect.bottom) {
      let childStyle = window.getComputedStyle(child);
      let scrollSnapAlign = childStyle.getPropertyValue('scroll-snap-align');
      console.log(child.innerText, scrollSnapAlign);
      parent.style.scrollSnapType = "none" === scrollSnapAlign ? 'y proximity' : 'y mandatory';
      break;
    }
  }
}
html {
  background: #f2f2f2;
}

.scroll-container,
.scroll-area-none,
.scroll-area {
  height: 100px;
  font-size: 60px;
}

.scroll-container {
  max-width: 850px;
  margin: 15px auto;
  overflow: auto;
  scroll-snap-type: y mandatory;
}

.scroll-area {
  scroll-snap-align: start;
}

.scroll-area-none {
  scroll-snap-align: none;
  background-color: black;
}

.scroll-area-none,
.scroll-area {
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
}

.scroll-area:nth-of-type(4n+1) {
  background: #49b293;
}

.scroll-area:nth-of-type(4n+2) {
  background: #c94e4b;
}

.scroll-area:nth-of-type(4n+3) {
  background: #4cc1be;
}

.scroll-area:nth-of-type(4n+4) {
  background: #8360A6;
}
<div class="scroll-container">
  <div class="scroll-area-none">1. -1</div>
  <div class="scroll-area-none">1. 0</div>
  <div class="scroll-area">1. 1</div>
  <div class="scroll-area">1. 2</div>
  <div class="scroll-area">1. 3</div>
  <div class="scroll-area">1. 4</div>
  <div class="scroll-area-none">1. 5</div>
  <div class="scroll-area-none">1. 6</div>
</div>

<div class="scroll-container">
  <div class="scroll-area-none">2. -1</div>
  <div class="scroll-area-none">2. 0</div>
  <div class="scroll-area">2. 1</div>
  <div class="scroll-area">2. 2</div>
  <div class="scroll-area">2. 3</div>
  <div class="scroll-area">2. 4</div>
  <div class="scroll-area-none">2. 5</div>
  <div class="scroll-area-none">2. 6</div>
</div>

Related