Skip

Smooth scroll swoosh

Here’s a quick tip for people who, like me, use this in practically every website project:

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

I love smooth scrolling and I remember very well how hard it was to get right, back in the day™. The animation helps users follow where an anchor link sends them, rather than jumping there instantly. Exactly the kind of purpose-driven animation I like.

While working on a recent project, I noticed an annoying issue with this native smooth scrolling related to loading URLs directly with anchors. For example a form that posts and reloads at its #form anchor. Instead of landing on the section, the page loads at the very top, pauses, and swoosh, animates all the way down.

This can be quite annoying and nauseating for long pages where it becomes more of a swooooooosh:

Alternative text description of the video After submitting a form, the page reloads at the very top and smoothly scrolls down quite the distance before reaching the same position again, this time with a “Thanks for signing” success message.

Firefox doing it right?

I opened the same URL in Firefox and it jumped straight to the section with no animation. So this apparently only happens in Webkit and Chromium, and it’s an ancient, known difference, documented in a Mozilla bug from over 20 years ago.

The reason is that scroll-behavior applies to any scroll “triggered by the navigation or CSSOM scrolling APIs”, which, I guess, includes that first jump to a #fragment on load.

The fix

Alternative text description of the video After submitting the form, the page reloads but stays in the same position: the form, now showing “Thanks for signing”.

I gate smooth scrolling behind a data attribute that is being added once the page has loaded:

@media (prefers-reduced-motion: no-preference) {
    /* Only enable smooth scrolling for <html> with data-smooth-scroll */
    :root[data-smooth-scroll] {
        scroll-behavior: smooth;
    }

    /* Also when no JavaScript is enabled */
    @media (scripting: none) {
        :root {
            scroll-behavior: smooth;
        }
    }
}
window.addEventListener("load", () => {
    requestAnimationFrame(() => {
        document.documentElement.dataset.smoothScroll = "";
    });
});

The first scroll happens while smooth is still off, so it jumps instantly. Every anchor click afterwards is smooth. And thanks to the scripting media query it also works without JavaScript, so nobody loses smooth scrolling, they just keep Chrome’s swoosh.

There’s also a CSS-only trick using :focus-within that switches smooth scrolling on only once something is focused, no JavaScript at all. It even stops browser find-on-page from scrolling smoothly (which I don’t really mind, but some people do). The catch, as Schepp documents: Safari doesn’t focus links on click, so it silently does nothing there for mouse users, and keeping the animation alive after a click needs an extra @keyframes hack. Too fiddly for my taste.

What do you think?

Got a nicer fix? Got an entirely different opinion about swooshes? You think Scroosh would have been a much better title for this article? Let me know on Mastodon:

  1. Don’t mistake this for scroll jacking; this is only about anchor link navigation scrolling smoothly.

Replies

No replies yet.