Routing
Route changes in single-page applications need to emulate the conventions of page loads
- Version:
- 0.1.0
- Status:
- Published
Introduction
Within a single-page application (SPA), the conceptual equivalent of pages are screens. Navigation between these screens is facilitated by a router. When the URL changes, the router dynamically fetches data and renders the content for the screen in situ. That is, unlike in a multi-page website, individual pages are not loaded; the single page is augmented.
While the unified structure of a single-page application is convenient for data persistence and state management, the routing behaviour can present issues for some users. When a new resource/page is loaded, two not insignificant events take place:
- Keyboard focus is moved to the body of the page, above all of the page's interactive content in terms of source order
- The page's title (
<title>
;document.title
) is announced in screen reader software, introducing the page
Most SPA routers do not emulate these expected behaviours, out-of-the-box. By default, focus is destroyed along with the previous screen's markup. Typically, focus then resets to the <body>
element, but without announcing the (changed) title. Sometimes the behaviour is more unpredictable — especially where the new screen takes some time to render. A 'ghost' focus may be maintained in the position where the underlying element was removed, meaning a a Tab keypress may focus a proximate element in the newly rendered screen.
The purpose of this document is to provide guidance on creating a more consistent and ergonomic behaviour.
Recommended markup
The route content
The screens of web applications, like the pages of websites, should share a consistent structure. Only the unique content for individual screens should change as the user is rerouted. The navigation should always be found in the same place, and offer the same navigation options.
The unique screen content should be housed in a <main>
element, making it accessible to screen readers via their landmark shortcut options. For example, in JAWS, the <main>
landmark is accessible by pressing the Q key[1].
<main id="main" tabindex="-1">
<!-- dynamically rendered screen content -->
</main>
Note the provision of tabindex="-1"
. The <main>
element should also be accessible by keyboard using a skip link. The skip link should be the first interactive element on the page, and allows keyboard users to bypass[2] the header and navigation where desirable.
<a href="#main">skip to content</a>
Recommended behaviour
Changing the title
Ensure the <title>
element is actually changed to reflect the newly appointed screen. Router packages typically support custom events for route changes. Employ a route change event to rewrite the document.title
. If such an event is not emitted, you may need to create your own, or listen to history
changes.
Guidance on writing descriptive <title>
s is covered in Headings. In short, they should be made up of a label for the page and a label for the site or application. This succinctly gives users all the context they need as they load (and switch between) tabs.
<title>Brexitcast | BBC Sounds</title>
The active link
It's important for wayfinding[3] that the current location is indicated. Customarily, this is done by highlighting the navigation link that corresponds to the current page. To make this indication accessible, use aria-current="page"
in place of a superficial class
. Links bearing aria-current="page"
are identified as "[link label], current page" is screen reader output.
In React Router 4, you provide the ariaCurrent
prop to each <NavLink/>
component. The aria-current
attribute appears where isActive
evaluates to true.
<NavLink to="/home" ariaCurrent="page">Home</NavLink>
Focus management
It's important focus is handled with deliberation, but what is done with focus depends on the circumstance. A common approach to handling focus between routes in a SPA is to focus the newly acquired screen's <h1>
element[4]. In plain JavaScript this would look something like the following.
const mainHeading = document.querySelector('h1');
mainHeading.tabindex = -1;
mainHeading.focus();
In React, you would defer to the refs
API[5]:
// Initialize <h1 ref={this.mainHeading} tabindex="-1">Brexitcast</h1>
this.mainHeading = React.createRef();
// Focus the ref's DOM node (accessible as `current`)
this.mainHeading.current.focus();