Accessible Routing for Sapper
On route-change, manage focus and update an ARIA Live Region with an indication of the current page.
Tags:
Alt. 1: Resembling the Traditional Website
Since implementing the Gatsby team’s recommendation, I now prefer an accessible SPA method that resembles a traditional website. Note that I will probably never test this to the extent the Gatsby team has done.
- On route change an app/site wrapper gets focus. The next focusable element is a skip link to main-element.
- The page’s h1 element is always present in the root layout with the attributes
role="region"
andaria-live="polite"
. The text is updated by using a store.
I made this to a component that must wrap the app. See implementation in my Sapper template.
Alt. 2: The Gatsby Teams’s Recommendations
Here is how to implement the Gatsby team’s recommendation for accessible SPA routing plus a “Skip to main content” link in a Sapper app. This works for both the exported static version without JS activated and the SPA version.
The Gatsby team’s main recommendations:
-
Provide a skip link that takes focus on a route change within the site, with a label that indicates what the link will do when activated: e.g. “skip to main navigation”.
- My implementation links to a skip link that skips to main content. - I also implemented said “skip to main content” link. -
Include an ARIA Live Region on page load. On a route change, append text to it indicating the current page, e.g. “Portfolio page”.
1. Skip Links
After testing with keyboard-navigation and Macos Voice Over, I arrived at the following code for skip links. The HTML and JS code is from the Sapper specific Svelte component _layout.svelte
.
HTML
First off, the top HTML layout looked like this:
<a
href="{fullPath}#main-content"
class="sr-only-focusable"
bind:this={skipToContent}
on:click|preventDefault={focusMain}
>
Skip to content
</a>
<header><!-- ... --></header>
<main id="main-content">
<!-- `tabindex="-1"` to not give focus when navigating by keyboard. -->
<a
href="{fullPath}#"
class="sr-only-focusable"
tabindex="-1"
bind:this={skipToTop}
on:click|preventDefault={focusTop}
>
Go to top of page
</a>
<slot></slot>
</main>
Note, the skip link for going back to top shows on focus. If you want to avoid that, another solution would be to still keep the link but give focus to the main-element (this requires that it has tabindex
).
Javascript
I used Svelte’s tick
function to not interfere with how Sapper handles new routes in the first place (like scrolling to the top automatically).
preventScroll
was added to focus
so Sapper and the browser can be smart about who handles scrolling. The browsers that supports preventScroll
will handle back/forward navigation from the browser like a normal non SPA-site (retain scroll position, etc.).
For the skip links to work without JS in the default Vercel config for Sapper, I added full paths to the skip links.
The script looked like this:
import { stores } from '@sapper/app'
import { tick } from 'svelte'
const { page } = stores()
let fullPath = ''
let skipToContent
let skipToTop
page.subscribe(value => {
fullPath = value.path
accessibleRouteChange()
})
async function accessibleRouteChange () {
if (!skipToTop) return
await tick()
skipToTop.focus({
preventScroll: true
})
}
function focusMain () {
if (!skipToTop) return
skipToTop.focus()
}
function focusTop () {
if (!skipToContent) return
skipToContent.focus({
preventScroll: true
})
}
CSS
The ‘classic’ screen reader only CSS classes (popularized by Bootstrap):
.sr-only,
.sr-only-focusable {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
border: 0 !important;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static !important;
width: auto !important;
height: auto !important;
margin: 0 !important;
overflow: visible !important;
clip: auto !important;
}
2. ARIA Live With Page Name
This can be implemented anywhere on an element that is always/already present in the layout—“add the attribute [aria-live] before the changes occur—either in the original markup, or dynamically”. The following attributes must at least be used on the element you choose: role="region"
and aria-live="polite"
.