Gatsby route changes focus the router wrapper instead of main content, breaking skip-link expectations
0 reputation · 15 Jun 2022, 15:11 UTC
0 reputation · 15 Jun 2022, 15:11 UTC
In a Gatsby site, navigating between pages moves keyboard focus to the router's wrapper element rather than to the main content region. For screen reader and keyboard-only users, this means a skip-navigation link that targets <main> behaves inconsistently: after a client-side route change, focus sits on the wrapper, and the next Tab press or skip-link activation does not land where users expect.
Gatsby uses @reach/router internally, and its documented accessibility behavior is to programmatically focus a wrapper div (with tabindex="-1") on route change so page transitions are announced. Navigation uses gatsby-link, so this focus management is active. The site also provides a standard skip link pointing at the main content landmark.
The tension is that the default focus target (the wrapper) and the skip link target (main content) are different elements, so the two accessibility mechanisms can work against each other. Behavior may also differ across Gatsby versions (v2 through v5), so any approach needs to be verified against the version in use and tested with NVDA and VoiceOver.
Gatsby’s internal focus manager (from @reach/router) moves keyboard focus to the first element it renders that has tabindex="-1" – usually a wrapper div – after each client‑side route change. This is intended to announce the navigation to screen readers, but it conflicts with a skip‑link that targets <main> because focus ends up on the wrapper instead of the main landmark.
onRouteUpdate, wrapPageElement, and the disable flag window.___focusManagementDisabled) is stable from Gatsby v2 through v5.<main><main> element can receive focus. Add tabindex="-1" to it (or make sure it already has a negative tabindex).gatsby-browser.js in the site root and export an onRouteUpdate function:export const onRouteUpdate = ({ location }) => {
const main = document.querySelector('main');
if (main) {
main.focus();
}
};
// gatsby-browser.js
window.___focusManagementDisabled = true;
export const onRouteUpdate = ({ location }) => {
const main = document.querySelector('main');
if (main) {
main.focus();
}
};
Either approach makes the router’s focus jump to <main> after navigation, keeping the skip‑link and the automatic focus in sync.
<Link> or using gatsby-link), open Chrome DevTools and run document.activeElement in the console; it should return the <main> element.<main>.gatsby --version in the project directory; the focus‑customization steps work the same from v2 to v5.Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.