I was browsing my next.js app with voiceover on, and noticed that client-side route changes aren't announced or even noticed by the screen reader. This is kind of alarming, since I've been using it in production on the assumption that the routing was accessible (my bad, was confusing with gatsby's use of reach-router).
I don't see many other references to "accessible routing" or "screen readers" in the issue history or documentation.
With voiceover (perhaps other screen readers too) on, click a next/link.
Focus should move to the top of the new page, and the new page title should be announced by the screen reader, like with a server-side route change, when clicking a next/link.
I'm not a regular screen reader user, so it's possible I'm missing something and the current implementation is actually fine, but I think the point is that the server behaviour and client-rendered behaviours should be the same?
It should focus back on the body already: https://github.com/zeit/next.js/blob/canary/packages/next/client/link.tsx#L183
cc @connor-baer
I'll have a look later today. The focus behavior _should_ work. The title announcement is new to me, but it sounds like a reasonable addition. I'll do a bit more research.
This issue should be reopened since #7693 was reverted by ##7753.
I've been doing some preliminary research on announcing client-side route transitions. Here's a good article that sums it up nicely: https://hiddedevries.nl/en/blog/2018-07-19-accessible-page-titles-in-a-single-page-app
My main takeaways:
<title>
element cannot be announced automatically, instead, an aria-live
region can be used to achieve the same effect.body
makes sense. However, in an application (e.g. like a dashboard with an app shell), you might want to retain focus on the current section or announce an element title, while the page title remains the same. IMO, Next.js should provide a reasonable default since many developers probably don't know or think about this. For advanced use cases, this behaviour should be customizable.
It would be super helpful to get an actual expert's opinion on this. I will also do more research and share my findings here.
We reverted the PR as it was merged right before Next.js 9 so it wasn't on canary long enough to be considered stable. @timer is going to re-apply it later.
_My 2¢ here (I'm by no means an accessibility expert):_
For arbitrary client-side navigation, it doesn't seem like it will be possible to know that a URL change indicates large content replacement, since that's highly application dependent.
As an example, imagine using a keyboard to navigate to a link in the header of a site. Would you expect focus to be redirected back to the full page? Going deeper, what if that link simply triggered a modal? Or more commonly, what if the link navigated to a page that was very similar to the current page, only changing a some small region of the screen contents?
Since this issue goes pretty deep on Accessibility concerns, I asked @robdodson. Rob wrote about similar things a couple years back, and I will try to paraphrase the suggestions he and I discussed:
Resetting focus back to starting point _is_ desirable, but you need to have a logical starting point to focus. Just blurring elements won't cause screen readers to announce new content, and focussing <body>
can be jarring for AT users.
So here's an alternative approach I'd like to float: give developers a convenient (and perhaps default) way of marking their focus root, then detect and focus it when performing client-side navigation. As a reference point, Gatsby appears to do this for their client-side navigation.
Here's a rough example of what might go on link.tsx
line 183:
// move focus back to the focus root if one has been provided:
const focusRoot = document.querySelector('[next-focus-root]');
if (focusRoot) focusRoot.focus();
That selector could be pretty easily added by developers, and potentially part of the default _app.js
:
/** @example
* const { Component, pageProps } = this.props
* <div id="app">
* <Header />
* <FocusRoot><Component {...pageProps} /></FocusRoot>
* </div>
*/
export function FocusRoot(props) {
return <div next-focus-root style="outline:none" tabindex="-1" role="group" {...props}>;
}
... or developers could annotate their desired focus root with the next-focus-root
attribute directly.
Just an update, Marcy happened to publish a post about this yesterday! It's definitely worth a read, and there's a whole section on the above:
https://www.gatsbyjs.org/blog/2019-07-11-user-testing-accessible-client-routing/#common-accessibility-barriers-in-client-rendered-web-apps
Blind regular screen reader user here! :) I was comparing reactJS and other server side rendering solutions for a project I am working on. Focus handling is a critical accessibility issue in most of the server side rendering solutions I've tested until now. I think that nextJS should automatically move focus on the first accessible element of the newly appeared component (the route destination) after completing the route transition.
A mechanism should be also provided to allow developers to easily indicate where focus should be moved after navigating to a new route to handle edge cases and/or customize the default behavior (perhaps even opting out from it) wherever it makes sense to do so.
Finally, when going back to a previously visited route (for instance using the browser's back button) focus should be automatically restored to the point it was before navigating to a new route (I would suppose 99.9% of the times the Link element that triggered the navigation event).
Feel free to ask me any question if you need more details; I'd be happy to test eventual patches, but don't feel confident enough with the framework for a PR (yet).
Hi! I was wondering if there was any plan to reintroduce this back into Next? We're having problems with screen readers not recognising that routing has taken place :/
After looking into this problem for a while, we noticed that next-router
already moves the focus to the body
on client side routing, but only if the body
can be focused, ie. has the tabIndex
attribute as -1.
However, having tabIndex
-1 present on body is not recommended, because it breaks the click+tab interaction. This interaction means clicking close to a link anywhere on the page and then pressing tab -> the link will receive focus. If body has tabIndex
-1, it will receive focus when clicking the body.
This can be worked around by adding the attribute on routeChangeComplete
and then removing it on blur. Here's our solution to this:
This is in our componentDidMount in
_app.tsx`:
Router.events.on('routeChangeStart', () => {
/*
* This is needed for the focus to be moved back to the beginning of the page after
* client-side routing by next-router, we'll set it programmatically and remove on blur
* because we don't want to focus the body if the user clicks on some parts of the body
* that are actually not clickable.
*
* This is useful for click+tab interaction, where you click close to a link + tab in order
* to focus that link – a fast way for moving the focus exactly where you want it to be.
*/
document.body.setAttribute('tabIndex', '-1');
});
document.body.addEventListener('blur', () => {
// See the longer explanation above about why we remove this
document.body.removeAttribute('tabIndex');
});
Feel free to send a PR to improve this!
Most helpful comment
Blind regular screen reader user here! :) I was comparing reactJS and other server side rendering solutions for a project I am working on. Focus handling is a critical accessibility issue in most of the server side rendering solutions I've tested until now. I think that nextJS should automatically move focus on the first accessible element of the newly appeared component (the route destination) after completing the route transition.
A mechanism should be also provided to allow developers to easily indicate where focus should be moved after navigating to a new route to handle edge cases and/or customize the default behavior (perhaps even opting out from it) wherever it makes sense to do so.
Finally, when going back to a previously visited route (for instance using the browser's back button) focus should be automatically restored to the point it was before navigating to a new route (I would suppose 99.9% of the times the Link element that triggered the navigation event).
Feel free to ask me any question if you need more details; I'd be happy to test eventual patches, but don't feel confident enough with the framework for a PR (yet).