Just Simply! Smooth Scrolling: Part One

In today's post, I'll be picking apart one of my favorite uphill battles: smooth scrolling. The post title is satire; smooth scrolling is only attractive shiny tip of the iceberg that clients want to see.

Smooth Scrolling is easy...right?

I can't begin to count the number of articles that advise using JavaScript to intercept and kill off click events in order to implement smooth scrolling. For example, approaches like this:

$('a[href^="#"]').click(function(event) {
  $('html, body').animate({
    scrollTop: $(this.hash).offset().top
  }, 1000);

This has a number of things wrong with it that completely breaks the native behavior that users expect when clicking on a jump link. This list is not comprehensive. See the HTML specification this link leads to an external website for a much more complete version of what should happen when navigating to a fragment.

  1. window.location is not updated
  2. Native focus management is lost
  3. There's no guarantee that the element that is being skipped to is even visible / accessible!
  4. What if the user has indicated that they prefer reduced motion?
  5. What if there's a sticky navigation banner? What if the sticky navigation banner changes height on resize or scroll? 😱

When presented with the above challenges, the developer reaction is almost always to keep shoveling on more JavaScript until someone realizes that a horrible decision has been made, but it's too late to go back.

window.location is not updated

Just simply use the history API!

Okay, well we can definitely push, replace, or pop state, but...

  1. Now don't we have to add a popstate event that duplicates our smooth scrolling function when a user tries to go back? Don't forget to also copy the check to make sure the user doesn't want reduced motion...
  2. What if the target element becomes inaccessible after getting pushed to history? Better also add a check in the popstate event to check...
  3. What if it becomes important to tap into hashchange events in the future? This event isn't emitted when using the History API. Do we have to emit a custom event instead?

Native focus management is lost

Easy! Use jquery animate's callback function to focus on the element after the scrolling animation completes!

Not so fast...

  1. What if the application utilizes :focus-visible? When javascript sets focus, it's treated as a keyboard interaction.
  2. What if the user hits tab while the animation is happening? Is it possible to cancel the scroll event?
  3. What if the target element isn't natively focusable? Better add a check in to update the tabindex to -1...ah, and don't forget to also make the change in the popstate event too!

What if the application utilizes :focus-visible?

Can't you just simply remove the focus ring if the user clicked the link and didn't use the keyboard?

Technically, yes...but I wouldn't recommend it. Some browsers ship with user preferences this link leads to an external website that can influence the :focus-visible heuristic. Trying to force this on users can quickly land a site owner in hot water with accessibility.

Enter feature creep

What you have works great, but we noticed that smooth scrolling does not happen when people visit our pages directly with anchors in the URL.


There's no guarantee that the element that is being skipped to is even visible / accessible

Tricky. Arguably, native browser functionality doesn't even address this point intuitively in all cases. In the next installment I'll share what seems to be a more reasonable, holistic, and accessible approach to improve things all around.