Architecting for Accessibility with Reach Router
Reach Router v1.3.x provides rank-based route matching and automatic focus management for WCAG 2.4.3 compliance. This guide covers its architecture, nested routing patterns, SSR strategy, and why new projects should evaluate React Router v6 instead.
06 Jun 2026, 11:40 UTC

Most React routers treat accessibility (a11y) as an afterthought. When a user clicks a link, the URL updates and focus often remains on the clicked element or resets to the body of the document, forcing screen reader users to manually navigate back to the new content. Reach Router (v1.3.x) solves this by treating focus management as a first-class routing concern, using rank-based matching to ensure predictable URL behavior.
The Rank-Based Matching Engine
Unlike routers that match paths based on the order of declaration in your code, Reach Router uses a specificity rank. This allows you to organize your route tree logically without worrying about a dynamic segment 'shadowing' a static one.
The router calculates priority based on these rules:
- Static segments (e.g., /about) have the highest priority.
- Dynamic segments (e.g., /user/:id) follow.
- Splat segments (e.g., /*) have the lowest priority.
In this architecture, the order of components does not matter; the router will always pick the most specific match available:
import { Router, Route } from "@reach/router";
const App = () => (
);
Focus Management and WCAG Compliance
The core advantage of Reach Router is its automated focus management. Upon a navigation event, the router moves focus to the first focusable element in the new view. This satisfies WCAG 2.4.3 (Focus Order) automatically.
However, sometimes you want to prevent the router from grabbing focus on specific elements, such as a persistent navigation bar. To handle this, Reach Router provides the data-reach-focus-guard attribute. Adding this to an element prevents the router from moving focus to it during a transition.
<!-- This element will be skipped by the router's focus logic -->
<nav data-reach-focus-guard>
...
</nav>
Nested Routing and Layout Boundaries
Reach encourages a hierarchical design where parent routes act as layouts. This creates a clear data boundary between global UI elements and page-specific content.
When using nested routes, the parent component must explicitly render the children prop. If you omit this, the nested routes will fail to render despite the URL matching.
const Layout = ({ children }) => (
My App
{children}
);
const App = () => (
);
Server-Side Rendering (SSR) Strategy
To avoid client-side hydration mismatches, the server-rendered HTML must match the initial client state exactly. Reach Router uses the ServerLocation component to inject the initial pathname into the tree.
import { ServerLocation, Router } from "@reach/router";
// On the server (e.g., Express handler)
const url = req.url;
const html = renderToString(
);
Operational Considerations and Limitations
While Reach Router is excellent for a11y, it is currently in maintenance mode. Architects should consider the following before starting new projects:
- Migration path: The Reach Router team recommends migrating to React Router v6, which adopted rank-based matching and focus management.
- No built-in data loading: Unlike React Router v6.4+ or Remix, there are no loader/action APIs for route-level data fetching, defer, or streaming SSR.
- Code-splitting is manual: Use
React.lazywithSuspensewrappers around route components; the router itself has no lazy-loading API. - Focus conflicts: Custom focus traps in modals or drawers can conflict with the router's focus management; apply
data-reach-focus-guardconsistently to opt elements out.
Verification Checklist
- Create a minimal React 18 app with
@reach/router@1.3.4, define a route tree with static, dynamic, and splat paths, and verify navigation order does not change which component renders. - Run axe-core or Lighthouse on a page with two routes to confirm focus moves to the new route's heading after each
Linkclick. - Build for production with code-split routes using
React.lazy+Suspenseand confirm the correct chunk loads on navigation without full-page reload. - Render the same route tree with
ServerLocation url="/nested/123"in a Node script and compare HTML output to client-rendered DOM after hydration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.