A Horizontal Scroll List and Custom Keyboard Navigation

Posted on November 15, 2021
Takes about 7 minutes to read

Getting started

It was time for a personal site refresh. I didn't plan much for this next iteration, but I knew I wanted to include a showcase of my CodePen projects. With so many to choose from, it was tough deciding on how I'd ultimately like to visibly display project content. To kick things off, a list of linked cards presented in a horizontal scroll container felt worthy of exploring.

The argument against a carousel-style UX popped in my head, naturally, and maybe I should not use a carousel. However, I wanted to try this aesthetic with the following scope in mind:

Below is a stripped-down CodePen demo focused on layout and keyboard navigation criteria:

Open CodePen demo

Overall, I'd consider this a horizontal scroll container. Not really a carousel. If you're shaking your head, disagree, and have feedback already, I'm looking forward to it! For the sake of getting to the real purpose of this article, let's read on.

User flow on a keyboard

After building out the page structure, I tried navigating the site homepage using my keyboard. I quickly noticed how tedious it was tabbing through every single one of those CodePen project links. Perhaps there's a way to make this interaction and page flow feel more seamless.

Let's jump into some solutions. The following is what I had tried with the latter option being the path forward.

My first solution was to introduce a "skip to next section" anchor element that would be focused prior to entering the project list. It's similar to skip navigation links, a common pattern for keyboard navigation and screen readers that allow us to jump directly to the site's main content area.

While inactive, this anchor element is visually hidden on the page. Once focused, the link appears on screen. We can then press the enter key and skip over these projects to the next page section containing the id used in the href attribute.

Using shift + tab to navigate back up the page will surface the same issue in reverse. At this point, I debated appending a skip link to the end of the project list. Doing so would lead to something like this:

<section id="above-section">
  <!-- section content -->
</section>

<a href="#below-section">Skip project section</a>
<ul class="projects">
  <!-- 40+ links -->
</ul>
<a href="#above-section">Skip project section</a>

<section id="below-section">
  <!-- section content -->
</section>

Hmm. This seems somewhat restrictive and may be confusing. Let's explore a different way to handle this navigation instead of sandwiching the component with these skip elements.

Custom keyboard control

This iteration explores setting focus on the project list element with tabindex. By customizing the tabindex on this component, we now have the choice of interacting with this list of links or jumping to the next focusable element on the page.

Here's how it works:

In an effort to better surface this interaction, helper text is inserted into the DOM when the container focus is visible. My screen reader testing has been limited to Voiceover on macOS at the time of writing this article, but it's good to note that with Voiceover enabled, we are given feedback on how to traverse the list using built-in keyboard shortcuts.

A screenshot of the projects list focused and the Voiceover notification
An example of the voiceover notification that reads, 'You are currently on a list. To move between items in this list, press Control-Option-Right Arrow or Control-Option-Left Arrow.'

One final tweak: Elements now scroll completely into view when focused. Without this bit of code, it was possible to focus an element overflowing the boundary of the viewport but it did not pull it all the way on screen. Combining the scrollIntoView method with a programmatic focus improves this flow:

const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
//...
selected.scrollIntoView({
  block: "nearest",
  inline: "start",
  behavior: reducedMotion.matches ? "auto" : "smooth",
});

selected.focus({ preventScroll: true });

Notice that a prefers-reduced-motion conditional is applied to the behavior option. This will respect our reduced motion settings and disable smooth scrolling of the list.

When JavaScript is disabled

This layout works as intended without JavaScript. The level of control I've added attempts to make it easier to interact with this component, but content is still navigable without it. You can give it a shot by disabling JavaScript in your browser settings. Navigating with your keyboard still works; You'll just have to tab through every project in the list. Mouse and touch scrolling are no different.

What's your take?

I've made quite a few assumptions here. Does this feel intuitive when navigating using a keyboard? Or is it possible that this may diminish the default flow? Your feedback will help me improve this experience or think about this component behavior differently. Reach out on Twitter or send me an email.

Helpful resources

Special thanks to my good friends that gave initial feedback in a draft of this article. Your help is very appreciated! Here are some other supportive resources:

Back to all blog posts