Gatsby accessibility has an unusual starting point: the framework already does more for you than most React stacks. Client-side navigation announces page changes to screen readers out of the box, and gatsby-plugin-image practically forces you to think about alt text. But better defaults are not the same as accessible, and the gaps that remain (focus that never moves, headings that lie about structure, decorative images with the wrong alt) are exactly the ones automated scanners and real users notice.
This guide is for Gatsby developers shipping production sites. It covers the four areas where Gatsby diverges from a plain React app: the built-in route announcer, focus management after navigation, alt text through GatsbyImage and StaticImage, and the semantic structure your page templates generate.
What Gatsby's route announcer does (and doesn't) do
In a server-rendered site, every link is a full page load, so screen readers re-announce the new document automatically. Gatsby's client-side router breaks that contract: clicking a <Link> swaps content via JavaScript and the screen reader stays silent. Gatsby compensates with a built-in route announcer, a visually hidden aria-live region that fires on every navigation and announces the new page's name.
You don't install a plugin for this; it ships in the core runtime and renders as <div id="gatsby-announcer">. It picks what to announce in priority order: the document <title>, then the first <h1>, then the pathname. That ordering is the whole game. If two routes share a generic title like "My Site" and have no <h1>, the announcer reads the same string on every navigation and a screen reader user has no way to confirm the page actually changed.
The fix is content, not config. Give every route a unique, descriptive <title> (via the Gatsby Head API in modern Gatsby, or react-helmet on older sites) and a meaningful <h1>. Then verify it: tab to a link, activate it, and listen. If you hear the new page name, the announcer is working. Silence means your title and heading are both empty or duplicated.
Focus management: the gap Gatsby leaves for you
Here is the part developers miss. Gatsby announces the new route, but it does not move keyboard focus. After navigation, focus stays on the link the user just activated, which no longer exists in the new page's content. The next Tab press often dumps the user back at the top of the browser chrome or somewhere unpredictable, and screen reader users lose their place. This is a WCAG 2.4.3 Focus Order (Level A) concern, and it is the most common keyboard defect on Gatsby sites.
The robust pattern is to move focus to a top-of-content target on each navigation. Render a focusable wrapper around your main content and send focus to it when the location changes:
const mainRef = useRef(null); useEffect(() => { mainRef.current?.focus(); }, [location.pathname]); return <main ref={mainRef} tabIndex={-1}>{children}</main>
Use tabIndex={-1} so the element is programmatically focusable but not a tab stop. A skip-to-content link as the first focusable element in your layout is the complementary half: it lets sighted keyboard users jump past the nav and gives you a reliable, visible focus target. See our notes on keyboard accessibility at /keyboard-accessibility for the interaction details, and never strip the focus indicator without a visible replacement.
- Wrap focus management in gatsby-browser.js or a layout component so it applies to every route, not page by page.
- Don't auto-focus a heading and also rely on the announcer reading that heading; test that the two don't talk over each other.
- Mind WCAG 2.2's new 2.4.11 Focus Not Obscured (Minimum, AA): after you move focus, make sure a sticky header doesn't hide the focused element.
Alt text with gatsby-plugin-image
The old gatsby-image package is deprecated; modern sites use gatsby-plugin-image with its two components, StaticImage for fixed assets and GatsbyImage for data-driven images from GraphQL. Both take an alt prop, and the plugin's ESLint rule warns when you omit it, a genuinely useful guardrail. But a lint pass only checks that alt exists, not that it is correct.
Two rules cover almost every case. For informative images, write alt that conveys the same information the image does in context: alt="Bar chart showing 2025 revenue up 40 percent", not alt="chart". For decorative images (background textures, dividers, an icon next to text that already says the same thing) pass an empty string, alt="", so the screen reader skips it. The mistake the linter cannot catch is passing alt="image" or repeating the filename to silence the warning; that adds noise without meaning.
GatsbyImage pulling data from a CMS is where alt quality slips, because the alt comes from a content field someone else fills in. Make that field required in your schema, surface it in the editor UI, and treat empty alt as a content bug, not a code one. If you need help with phrasing, the alt-text checker at /alt-text-checker flags weak or missing alt. Note that GatsbyImage also renders a low-resolution placeholder and a wrapper div; confirm the placeholder doesn't expose a duplicate or stale alt during loading.
Semantic structure in page templates
Because Gatsby generates pages from templates and MDX, a single structural bug multiplies across hundreds of URLs. The two that matter most are landmarks and heading order. Every page should expose one <main>, a <nav>, and a <header>/<footer> as appropriate, so screen reader users can jump between regions. If your layout wraps children in a plain <div>, assistive tech sees an undifferentiated blob.
Heading order is the other multiplier. Each page needs exactly one <h1>, and levels must not skip; an <h2> followed by an <h4> breaks the outline screen reader users navigate by. This goes wrong in Gatsby when a reusable component hardcodes a heading level that's correct on one template and wrong on another, or when MDX authors drop in a #### because they liked the size. Drive heading level from a prop or the MDX components map, and validate the rendered output rather than the source. The heading checker at /heading-checker audits the result.
- One <h1> per page, sourced from the page's real title, the same string your route announcer reads.
- Use landmark elements (<main>, <nav>, <aside>) instead of <div> so regions are navigable.
- Style headings with CSS, not by choosing a level for its size; level encodes structure.
- Run an automated pass with AccessScan, then keyboard- and screen-reader-test the templates that generate the most pages.
Why this matters beyond the audit
Accessibility is increasingly a legal baseline, not a nice-to-have. Under the European Accessibility Act (Directive (EU) 2019/882), which applies from 28 June 2025, covered services such as e-commerce and consumer banking must meet WCAG 2.2 Level A and AA through the EN 301 549 standard, enforced by national laws like Germany's Barrierefreiheitsstärkungsgesetz (BFSG), France's RGAA, and Italy's Legge Stanca. A Gatsby storefront or banking front end in scope is judged on the same criteria covered here: focus order, name/role/value, and meaningful images.
The good news is that Gatsby's defaults get you part way for free. Spend your effort on the gaps (focus after navigation, real alt text from your CMS, and honest heading structure) and run a scan to catch regressions before they ship. Drop a URL into the free AccessScan checker at /#scan to see where your Gatsby build stands against WCAG 2.2.