Google Chrome: Simultaneously 'smooth' scrollIntoView() with more elements doesn't work
Asked Answered
D

3

26

In Google Chrome, element.scrollIntoView() with behavior: 'smooth' doesn't work on multiple containers at the same time. As soon as smooth scrolling is triggered on one container, the second container stops scrolling. In Firefox, this problem doesn’t exist; both containers can scroll simultaneously.

My workaround is using behavior: 'instant', but I like to use behavior: 'smooth' for a better user experience.

Example

Here is a plunker using Angular

html

<p>
  In Google Chrome element.scrollIntoView() with behavior 'smooth' doesn't work, if scrolling more containers at the same time.
  Shwon in case 'All Smooth (200ms sequence)' the container stopps scrolling.
  <br>
  <br> In Firefox all works.
</p>

<div class="row mb-1">
  <div class="col">
    <button (click)="reset()" type="button" class="btn btn-secondary">Reset</button>
  </div>
</div>

<div class="row mb-1">
  <div class="col">
    <button (click)="scrollAllInstant()" type="button" class="btn btn-secondary">All Instant</button>
    <small class="text-success">Works</small>
  </div>
</div>

<div class="row mb-1">
  <div class="col">
    <button (click)="scrollAllSmooth()" type="button" class="btn btn-secondary">All Smooth (simultaneously)</button>
    <small class="text-danger">Only one container is scrolled</small>
  </div>
</div>

<div class="row mb-1">
  <div class="col">
    <button (click)="scrollAllSmoothSequenced()" type="button" class="btn btn-secondary">All Smooth (200ms sequence)</button>
    <small class="text-danger">Only last container is scrolled to 85 - Others will stop, if next container is triggered</small>
  </div>
</div>

<div class="row">
  <div *ngFor="let container of containers, let index = index" class="col">

    <button (click)="scrollSingelContainer(container)" type="button" class="btn btn-secondary mb-1">Single Container Smooth</button>
    <small class="text-success">Works</small>

    <div class="card bg-light mb-3" style="height: 500px;  max-width: 18rem;">
      <div class="card-header">Container {{ container }}</div>
      <div (scroll)="onScroll(container)" class="card-body" style="overflow-y: scroll;">
        <p *ngFor="let number of content" [attr.id]="container + '_' + number" class="card-text" [class.text-danger]="number == 85">{{ number }}</p>
      </div>

    </div>
  </div>
</div>

typescript

export class App {
  name: string;

  containers = [0, 1, 2]
  content = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]

  constructor() {
    this.name = `Angular v${VERSION.full}`
  }

  private scroll(container: number, row: number, behavior: string) {
    let element = document.getElementById(container + '_' + row);
    element.scrollIntoView({
      behavior: behavior,
      block: 'start',
      inline: 'nearest'
    });
  }

  reset() {
    this.containers.forEach(container => {
      this.scroll(container, 1, 'instant');
    });
  }

  scrollSingelContainer(container: number) {
    this.scroll(container, 85, 'smooth');
  }

  scrollAllInstant() {
    this.containers.forEach(container => {
      this.scroll(container, 85, 'instant');
    });
  }

  scrollAllSmooth() {
    this.containers.forEach(container => {
      this.scroll(container, 85, 'smooth');
    });
  }

  scrollAllSmoothSequenced() {
    this.containers.forEach(container => {
      setTimeout(() => {
        this.scroll(container, 85, 'smooth');
      }, 200 * container);
    });
  }

  onScroll(container: number) {
    console.log('Scroll event triggerd by container ' + container);
  }
}
Decile answered 16/3, 2018 at 10:29 Comment(1)
I opened bugs.chromium.org/p/chromium/issues/detail?id=1121151Encrata
A
17

A similar question was asked here: scrollIntoView() using smooth function on multiple elements in Chrome, but the answer is not satisfying, as it states, that this isn't a bug but.

But it seems to be a bug and it is already reported in the Chromium bug list:

For smooth scrolling of multiple elements at the same time using scrollIntoView (for at least some of the elements) we need to wait for a fix from the Chromium team.

An alternative approach is using scrollTo which is working for multiple elements also in Chrome. See scenario 5 in this example: https://jsfiddle.net/2bnspw8e/8/.

The downside is that you need to get the next scrollable parent of the element you want to scroll into view (see https://stackoverflow.com/a/49186677 for an example), calculate to offset which is needed to scroll the parent to the element and call parent.scrollTo({top: calculatedOffset, behavior: 'smooth'}).

Astronomy answered 24/8, 2020 at 14:52 Comment(2)
Took me so long to understand why this was not working, thanks for the solution !Affine
The math in scenario 5 of your scrollTo example didn't seem quite right to me. I wound up with: const calculatedOffset = elem.offsetTop - container.offsetTop - container.getBoundingClientRect().height/2 + elem.getBoundingClientRect().height/2;Vinitavinn
H
1

If you want a workaround for Chrome that uses smooth whenever possible (when only a single element is being scrolled into view, but falls back to auto when needed to make sure multiple elements can be scrolled at the same time (and without having to use scrollTo), you can do something like this:

const IS_CHROME = navigator.userAgent.includes('Chrome')

let lastScrolledElement = null
let timeoutID = 0

function scrollElementIntoView(element) {
  if (!element) return

  if (IS_CHROME && lastScrolledElement) {
    lastScrolledElement.scrollIntoView({
      behavior: 'auto',
      block: 'nearest',
      inline: 'center',
    })
  }

  lastScrolledElement = element

  window.clearTimeout(timeoutID)

  timeoutID = window.setTimeout(() => {
    lastScrolledElement = null
  }, 250)

  element.scrollIntoView({
    behavior: 'smooth',
    block: 'nearest',
    inline: 'center',
  })
}

You could even remove the IS_CHROME check if you want to avoid browser sniffing and just have this enabled for all browsers.

This way, if you scroll element A into view and after a second or two you scroll element B into view, both will look scroll smoothly, as by the time a.scrollElementIntoView(...) is called the second time, it would have scrolled already, and the timeout would have been cleared.

However, if you call scrollElementIntoView 2 times consecutively, when the second call is made, the first element would probably be still scrolling into view. Therefore, we force it to scroll immediately (with behavior: auto) before calling scrollIntoView for the second element (which will scroll smoothly).

Hekate answered 18/2, 2023 at 5:2 Comment(0)
V
0

This "ponyfill" of smooth scroll behavior (and other not currently spec behaviors) does work on Chrome and is not subject to the Chrome bug (thanks others to linking to confirmation of Chrome bug).

https://github.com/scroll-into-view/smooth-scroll-into-view-if-needed

Vinitavinn answered 29/5 at 17:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.