Better scrolling through modern CSS

Better scrolling through modern CSS

There's more to scrollbars than you might expect

ยท

8 min read

Featured on Hashnode
Featured on daily.dev

Scrollbars. A very mundane thing that every website and app has. Historically, it has been difficult to consistently customize scrollbar styling on the web. So it is understandable that frontend developers don't pay much attention to them. While it's a good thing that most of us (hopefully) aren't writing thousands of lines of JavaScript to recreate scrollbars, we should be paying at least a little more attention to them. Let's look at some of the things we can do in 2022 to improve the scrolling experience for our users.

The basics

When the content of an element is too big to fit in it, we can use overflow: auto to make it scrollable. There's more to it, but this is all you need to make the default scrollbars show up.

As for styling the scrollbars, you can use the -webkit prefixed scrollbar properties combined with the standard scrollbar-width and scrollbar-color properties. There have been countless posts on this topic, so I won't bother repeating those parts. I recommend Ahmad Shadeed's scrollbar guide if you're not already familiar with these properties.

I do want to mention the postcss-scrollbar plugin that generates the -webkit prefixed scrollbar properties from the standard ones. Which means you can get cross-browser styles with just this:

.scroll-container {
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: hsl(0 0% 50%);
  /* postcss-scrollbar will add the -webkit version automatically! */
}

Looks simple enough. Now let's get to the more interesting bits.

Theming scrollbars

If you've decided you still want to rely on the default scrollbars, fair enough. But you need to be careful here if your site supports a dark theme. I see way too many sites implement a dark mode with their scrollbars still stuck in light mode. Here's what the remix.run docs look like on Windows in dark mode:

screenshot of remix docs showing a minimal dark theme with ugly white scrollbars

This can be fixed using the color-scheme property or meta tag. You should probably be doing this anyway, for non-scrollbar reasons too. Thomas Steiner has a great article on web.dev if you want to learn more.

html {
  /* defer to OS preference */
  color-scheme: dark light;

  /* override, assuming the theme toggler sets a data-theme attribute */
  &[data-theme=light] { color-scheme: light; }
  &[data-theme=dark] { color-scheme: dark; }
}

The scrollbar quirks of various operating systems

In this section, I will rant about different environments, the expectations, the behaviors, and the problems that come with them, as well as some future speculation. If all you care about is the code, feel free to scroll to the end of this section. ๐Ÿ˜…

Windows

As I demonstrated in the previous section, Windows has always had some of the ugliest-looking scrollbars of any platform. Windows 10 specifically has the ugliest scrollbars in all browsers. Even if you set the right color scheme, they look too boxy and thick.

Windows 11 is attempting to modernize the scrollbars by making them thinner and overlay. This can be noticed in the various first-party interfaces (e.g. the Settings app) across the OS. As for the browsers, Firefox 100+ is already shipping it, while Edge/Chrome have it in some form behind a flag.

The Firefox implementation in particular worries me because the scrollbar is razor thin and disappears unless you're actively interacting with it. To make things worse, there seems to be no way to change this behavior programmatically. Firefox does respect the "Always show scrollbars" setting in Windows 11 but it's off by default and we can't reasonably expect most users to turn it on. I really hope this will be addressed in a future update. For now we can't do anything.

On Chromium, things are simpler: if you specify even one of those -webkit-scrollbar prefixed properties, you become responsible for providing the entire scrollbar styling. This is exactly what we want, at least until the Windows 11 scrollbars ship in Chromium.

macOS

While macOS scrollbars look less offensive, they come with their fair share of problems.

The first one is a bit of nitpick but I find it super frustrating that I can't just move my cursor to the right edge of a browser window and use it to scroll. This is because macOS reserves the edges for resizing/moving the window, even if the cursor is still hovering on the scrollbar thumb. This effectively makes the already-thin scrollbar even thinner (albeit only for the viewport scroller).

The second is a more relatable problem: automatically disappearing scrollbars. They can offer a nice and clean interface... if the user knows that a part of the page is scrollable. In practice, they are very frustrating to deal with. Recently I had an experience where I literally couldn't find the ticket to a conference because it was hidden outside the scrollport and the scrollbar was invisible. In other words, disappearing scrollbars can literally cost you money. To work around this, you could maybe force the height of the scrollport to be such that the last item is half visible. Or you could use some JavaScript to detect scroll position and add a scroll shadow.

Both of these issues can be avoided by specifying custom scrollbar styling, so we should be good here.

Android, iOS, and touchscreens

Touchscreen devices are interesting. They have the same thin, overlaying, disappearing scrollbars that I framed as a problem in macOS, but the usage is quite different: the user doesn't interact directly with the scrollbar and instead drags their thumb against the scrollport. Also, because the screen is usually smaller, content overflows more often and the user is expected to scroll a lot more to find content. This is also why there are special features designed specifically for this modality, such as (1) momentum-based (inertia) scrolling, and a (2) visual highlight or spring animation when reaching the edge of a scrolling container. Android is even more thoughtful about this and decides to always show the scrollbar when a scrollable element first appears, and then switch to the regular auto-disappearing behavior after the user has scrolled at least once (signalling that they are now aware the area is scrollable).

All that to say, I quite like the default behavior of these scrollbars and would like to preserve it. This is achievable using the pointer media query, which will let us conditionally style scrollbars based on whether the user is on a high-accuracy pointing device (e.g. mouse) or a low-accuracy one (e.g. touchscreen).

@media (pointer: fine) {
  .scroll-container {
    /* ...custom scrollbar styles only for desktop */
  }
}

I've used this approach on multiple projects and feel pretty good about the results.

Preventing layout shift

One problem with overflow: auto is that it only shows the scrollbar if the content is actually overflowing, meaning it can cause a layout shift if a scrollbar suddenly appears. Historically, this has been possible to fix using overflow: overlay (in Chrome) to avoid having the scrollbar ever occupy space, and/or using overflow: scroll to always have it occupy space.

These days we can explicitly tell the browser to reserve space for scrollbars using scrollbar-gutter: stable. It's supported in Chrome and Firefox, and we can fallback to overflow: scroll for Safari. If you use an invisible scrollbar track, this fallback should work identically to scrollbar-gutter.

.scroll-container {
  overflow: scroll;

  @supports (scrollbar-gutter: stable) {
    overflow: auto;
    scrollbar-gutter: stable;
  }
}

Worth noting here is that if you want scrollbar-gutter on the viewport, it's tricky to make it work on the <body> and might be easier to move it either to the :root or to a child.

More scrollbar goodies from modern CSS

While not directly related to scrollbar styling, there are a few more properties I want to briefly mention that can help improve the scrolling experience. The wealth of tools CSS provides us today never ceases to amaze me.

Scroll padding

scroll-padding allows creating an offset at the edge of the scrollport. This is incredibly useful when you have a sticky header that would otherwise cover up headings scrolled to via in-page jump links or fragment urls.

.scroll-container {
  scroll-padding-top: var(--header-height);
}

There's also scroll-margin which behaves similarly, except it is applied on the individual elements rather than the scroll container.

Scroll behavior

scroll-behavior lets you enable smooth scrolling, which is also super neat for in-page jump links. Don't forget to guard it in a reduced motion check for better accessibility!

@media (prefers-reduced-motion: no-preference) {
  .scroll-container {
    scroll-behavior: smooth;
  }
}

Overscroll behavior

Named similarly as the previous one, overscroll-behavior does something quite different: it lets you prevent scroll-chaining in nested scrolling areas. This is especially handy in common nested regions like dialogs and sidebars, where you don't want the underlying page to start scrolling when you reach the end of the current scrolling container.

.scroll-container {
  overscroll-behavior: contain;
}

From a UX perspective, I think contain should be the default for all scrollable regions, and auto should be used in very few places.

Scroll snapping

Introduced a while ago, CSS scroll snapping has gone through numerous improvements and patches. Today it is a fairly reliable way of building scrollable components without a lot of JavaScript.

This is what it looks like in its most basic form (not that you would ever build a carousel... right?):

.scroll-container {
  scroll-snap-type: x mandatory;

  & > * {
    scroll-snap-align: start;
  }
}

There are a lot more interesting things you can do with scroll snapping, especially when combined with some of the other properties mentioned above. Did you know you can use it to build a stories component or a sliding menu?

I also highly recommend watching Adam Argyle's excellent talk titled "Oh Snap!" to see even more cool things that you can do with scroll snapping.

Conclusion

I could rant about scrollbars all day, but that wouldn't be very respectful of your time. So if you've managed to come this far, I just want to say thanks for reading and I hope this inspires you to go forth and build better scrolling experiences. If you have any thoughts/questions, feel free to reach out! ๐Ÿ‘‹