loading...
Cover image for scrollIntoView is the best thing since sliced bread

scrollIntoView is the best thing since sliced bread

steveblue profile image Steve Belovarich Updated on ・4 min read

Making elements scroll into view used to be hard, especially with animation. Now it's super easy with Element.prototype.scrollIntoView!

I've been working on an online art gallery for @sueish. She is an amazing artist. Curators need to focus on her artwork, so going for the super minimal look.

I need the digital equivalent of walking through a gallery so here I am again building an image carousel. I tried to think of all the times I coded a carousel but then I remembered I lost count a long time ago. There was one point an image carousel I built was used on every lexus.com car model page in the US.

Years ago it took a lot of code to animate the slides in and out of view. The solution involved some simple math and a mechanism for dynamically calculating width of each slide and the overall container. Once you figured out where the position the carousel started and where it had to land, you had to implement a way to tween the position of the element. It took several lines of code. Now all you need is one.

document.querySelector('some-element').scrollIntoView({behavior: 'smooth'});

The above line of code selects an element and animates it scrolling into view.

I'm coding this app with Angular just because that's what I use all the time. I have been venturing into web components lately, but I need to ship this site quick. The template of my Angular Component looks like this.

<img class="ctrl --left" src="/assets/arrow-left.svg" (click)="slide('-')">
<img class="ctrl --right" src="/assets/arrow-right.svg" (click)="slide('+')">

<div class="gallery" #gallery>
  <div class="slide" #slide *ngFor="let media of (media$ | async)" >
    <img [attr.src]="fetchImage(media.filename)" />
  </div>
</div>

There's some stuff going on here. Event listeners for click are bound to the left and right buttons. ngFor loops through a data model, an array of Media. I'm using the async Pipe because media$ is an Observable. In a service an http request fetches the data model and this component is subscribed to the response. The src of each image gets set by a property on the Media object.

I select each slide with ViewChildren in my Angular component.

@ViewChildren('slide') slides: QueryList<ElementRef>;

In the template I tagged each div with #slide, allowing me to select the slide elements.

When the user clicks either arrow button, the slide method is called on my component.

<img class="ctrl --left" src="/assets/arrow-left.svg" (click)="slide('-')">
<img class="ctrl --right" src="/assets/arrow-right.svg" (click)="slide('+')">

In the slide method we keep track of the current index and call the animate method, making sure the user can't go past the first slide into negative territory or beyond the last slide.

 slide(ctrl: string) {
    if (ctrl === '-') {
      if (this.index > 0) {
        this.index = this.index - 1;
        this.animate('-');
      }
    }
    if (ctrl === '+') {
      if (this.index < this.length - 1) {
        this.index = this.index + 1;
        this.animate('+');
      }
    }
  }

When I started coding this carousel I assumed animate would need to know direction. If you remember from earlier, it used to take a lot of math to animate a slide. The positive or negative direction was essential to figure out if the carousel needed to move left or right. Turns out scrollIntoView doesn't need to interpret the direction. scrollIntoView just animates in the element.

This is the method I ended up with that animates the slides in this carousel.


animate() {
  this.slides.toArray()[this.index].nativeElement.scrollIntoView({behavior: 'smooth'});
}

For a complete reference of Element.prototype.scrollIntoView, visit MDN.

I just think this scrollIntoView API the best thing since sliced bread. At least if you ❀️ to carb it up like I do. Animating an element scrolling into view is a very nice way to provide feedback to the user. We get this behavior practically for free with Element.prototype.scrollIntoView. Like all other bright and shiny new APIs I am left to wonder if I can use this in every browser.

Looking at the Can I use table nearly every browser shows only partial support, having left out the only feature I really care about: the smooth behavior!

Luckily there is a polyfill that fixes this behavior until browsers widely support 'smooth', that is if they ever do.

There's still some work to do on this carousel, but so far I am impressed I could animate images so quickly. There is a cost to this approach. The native scrollbar remains. This carousel won't look so minimal in Windows. Looks like I am back to figuring out a way to transition slides once again. Good thing we have the Web Animations API.

To be continued...

Posted on by:

steveblue profile

Steve Belovarich

@steveblue

full stack web engineer, creative coder, teacher, cultural critic and indie music fan.

Discussion

markdown guide
 

Unfortunately scrollIntoView doesn't seem to behave consistently across platforms, and I have no idea why. I also created a horizontally scrolling image carousel embedded in a feed page. Maybe I created an edge case with a certain layout where it happens to be inconsistent (boring details: I wanted it to be touch-scrollable on mobile devices, which requires a CSS hack that basically hides the scroll bars by setting their width / height to 0).

Here are my observations. I tested scrollIntoView using Google Chrome on: Windows 10, Mac OS X and Linux (KDE Neon, based on Ubuntu).

The best overall scroll appearance is on Mac OS X. The page does not move much vertically and the carousel item is correctly brought to the center of the view.

Windows 10 is close, but it tends to scroll the carousel view to the top of the browser view port, which is a bit disorienting.

Linux (latest version of KDE): it doesn't work properly. The carousel view is scrolled to the top of the view port, but the carousel item is not brought to the center of the view and remains hidden.

No idea why the same browser would behave in different ways. Maybe there is some kind of native UI dependency that causes different results. I assume it's something to do with window logic and scrollbar rendering, which is probably routed to native UI rendering.

Either way, I will probably have to recreate the scroll logic myself the exact way I need :-(

Unless you know what might be wrong and how to fix it?

 

FWIW the post was about getting excited about a new API only to be disappointed.

I haven't experienced the issues you are having in Windows 10 Edge. The carousel animates in fine for me. Testing in Ubuntu 18.04 theres definitely some issues with vertical scrollbars that overflow-y: hidden fixes.

Here is my SCSS if it helps.

A tip for iOS Safari.

  -webkit-overflow-scrolling: touch;

You can set the look and feel of the scrollbar in Windows 10 to react similar to MacOS, where it autohides.

  -ms-overflow-style: -ms-autohiding-scrollbar;

This carousel is styled for a full page takeover, so set width and height to fit your container.

:host {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  height: 100vh;
  -webkit-overflow-scrolling: touch;
  -ms-overflow-style: -ms-autohiding-scrollbar;

  .ctrl {
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    cursor: pointer;
    &.is--left {
      left: 0px;
    }
    &.is--right {
      right: 0px;
    }
  }

  > .gallery {
    height: 100vh;
    display: flex;
    flex: 0 0 auto;
    > .slide {
      width: 100vw;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      img {
        width: auto;
        height: auto;
        max-height: 60%;
        max-width: 60%;
        opacity: 0;
        transition: opacity 0.5s ease-out;
        &.is--visible {
          opacity: 1;
        }
      }
    }
  }
}
 

Hmm... So the main difference in your case is that your carousel is full screen, which rules out vertical scrolling.

My CSS rules for the container are not too far off from yours:

overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;

My carousel is embedded in an infini-loading feed page, so vertical scrolling is an issue with that method.

<...short break for checking stuff....>

Sooooo.... I just tested Firefox on Ubuntu and it behaves the exact same way as Firefox on Windows and Mac OS X as well. It works. Kinda. So the horizontal scroll that brings the image into view does work there, but overall the carousel also scrolls to the top of the view port.

Out of curiosity I tested the latest Google Chrome on my old Mac Mini again. And lo and behold: Google Chrome is broken across Linux and Mac OS X (haven't tested Windows yet). scrollIntoView does not work anymore as expected. Well... it does scroll the entire carousel to the top of the view port, but the image is not brought into view. Looks like Google messed up something recently πŸ˜”

The carousel being full screen makes no difference. If there is overflow of the container, vertical scrollbar will appear. There can still be issues with vertical scrollbars showing up in Ubuntu in particular without any apparent overflow.

I haven't seen this same issue with "the carousel also scrolls to the top of the view port." I've been testing in Chrome Canary with good results.

Ok, I just figured out what my issue is. I'm an idiot who doesn't read the documentation.

I ignored the options arguments inline and block so far. When I implemented my carousel on Mac OS X a couple months ago I only specified 'behavior': 'smooth' and skipped the other two properties. It gave me perfect results back then, and that seems to have changed recently (change of default values?).

Adding the two missing properties fixes the problem though. I get consistent results and scroll behavior in Firefox and Chrome and on both Linux and Mac:

child.scrollIntoView({behavior: 'smooth', inline: 'center', block: 'center'});

Ugh... Sorry for having been such a Debbie Downer here. On the other hand that little conversation with you made me actually look a bit harder and fix the problem in the end. I was about to roll my own scrollIntoView implementation because it looked totally broken to me.

So well... yay, scrollIntoView is actually awesome πŸ˜„

For anyone else coming by: please ignore this thread

nothing to see

Actually, this sub-thread detour specifically just helped me learn about this for a Vue app, so thanks for that everyone :)

 
@ViewChildren('slide') slides: QueryList<ElementRef>;

Didn't let me access the nativeElement of 'slides'

had to use:

@ViewChildren('slide', { read: ElementRef }) slides: QueryList<ElementRef>;