Learning path · Level 10 · Tutorial 1

Build Accessible Client-Side Routes with React Router

Create multiple URL-addressable views in a local React application with persistent navigation. Make page changes understandable through active links, document titles, and deliberate keyboard-focus behavior.

  • Level: Intermediate
  • Duration: 25 min
  • 8 steps
Start tutorial Back to the path
Build Accessible Client-Side Routes with React Router

What you will go through

  1. Connect URLs to React Views Build a local Field Notes application in which BrowserRouter, Routes, and Route map three static browser URLs to React views. 4 min
  2. Keep a Shared Layout Around Changing Views Restructure the static routes beneath a persistent shared layout with an index child and an Outlet. 4 min
  3. Navigate with Real Links Add persistent Link-based primary navigation and verify client-side history alongside native browser link actions. 3 min
  4. Recover from an Unmatched URL Add an in-app fallback for unsupported URLs while preserving the shared layout and a route back to Home. 2 min
  5. Make the Current View Clear Use NavLink to show the current destination with aria-current while keeping keyboard focus visibly separate. 3 min
  6. Synchronize the Browser-Tab Title Synchronize meaningful browser-tab titles with the current React Router pathname, including canonical static paths and unmatched URLs. 3 min
  7. Move Focus Deliberately After Navigation Add a route-change focus policy that orients keyboard users without overriding initial browser focus. 4 min
  8. Check the Complete Navigation Experience Validate the completed Field Notes routes through direct URL entry, keyboard navigation, browser history, reloads, native link actions, and unmatched-path recovery. 3 min

What you will learn

  • Configure nested routes with a shared layout, an index view, and an unmatched-route fallback.
  • Navigate between views with links while preserving browser history and native link actions.
  • Expose the current view through active navigation and a meaningful document title.
  • Check keyboard focus after page navigation, direct URL entry, and browser Back or Forward.

Before you start

  • Build a Local React App with TypeScript
  • Compose Interfaces with Typed Props
  • Focus Native Controls with React Refs
  • Synchronize External Systems with React Effects

Step 1 of 8

Connect URLs to React Views

Build a local Field Notes application in which BrowserRouter, Routes, and Route map three static browser URLs to React views.

Map the address to a view

URL-to-view routing

A client-side router reads the browser URL and chooses a React view. During ordinary in-app navigation, React can replace the rendered view without requesting a new HTML document.

In this first version of Field Notes, three static URLs map directly to three view components:

  • / renders Home
  • /guides renders Guides
  • /about renders About

BrowserRouter supplies the routing context. Inside it, Routes examines the current URL and Route connects a path to an element.

A diagram showing browser URLs flowing through a router to three different React views.

The router uses the current browser address to select one rendered view.

Add the router and route declarations

Create and run the project

Create the local project, install React Router v7, and start Vite. If you already have a Vite React TypeScript project named field-notes, run the last two commands from that project instead.

Then replace the generated src/main.tsx and src/App.tsx files with the source on this screen.

Terminal commands

Run these commands in your terminal.

bash
npm create vite@latest field-notes -- --template react-ts
cd field-notes
npm install
npm install react-router@7
npm run dev
Expected output
> field-notes@0.0.0 dev
> vite

  VITE v7.0.0  ready in 180 ms

  ➜  Local:   http://localhost:5173/
  ➜  press h + enter to show help

The Vite version and local port can differ. Open the Local address printed by your terminal.

src/main.tsx

src/main.tsx mounts one BrowserRouter around the application. Keep this router at the application entry point.

tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";
import "./styles.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

src/App.tsx

src/App.tsx declares the three static path-to-view associations.

tsx
import { Route, Routes } from "react-router";
import { AboutView, GuidesView, HomeView } from "./views";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<HomeView />} />
      <Route path="/guides" element={<GuidesView />} />
      <Route path="/about" element={<AboutView />} />
    </Routes>
  );
}

Add the three static views

Create the visible views

Add the view components and small stylesheet below. Each component supplies a distinct h1, making the selected view easy to confirm.

At this stage, type a supported path directly into the browser address bar after the local server address. For example, use the path for the Guides view. The router should render the matching component.

View components

src/views.tsx

tsx
export function HomeView() {
  return (
    <section>
      <h1>Home</h1>
      <p>Welcome to Field Notes.</p>
    </section>
  );
}

export function GuidesView() {
  return (
    <section>
      <h1>Guides</h1>
      <ul>
        <li>Layout basics</li>
        <li>Keyboard access</li>
      </ul>
    </section>
  );
}

export function AboutView() {
  return (
    <section>
      <h1>About</h1>
      <p>Field Notes collects practical interface guidance.</p>
    </section>
  );
}

Base styles

src/styles.css

css
:root {
  font-family: Arial, sans-serif;
  color: #18212f;
  background: #f5f7fa;
}

body {
  margin: 0;
}

section {
  max-width: 42rem;
  margin: 4rem auto;
  padding: 2rem;
  background: #ffffff;
  border-radius: 0.75rem;
  box-shadow: 0 0.25rem 1rem #18212f1a;
}

h1 {
  margin-top: 0;
}

Verify direct URL matching

Practice: declare and verify the static routes

Complete this static route declaration. Then use your local browser to directly enter each of its supported paths and verify that the rendered route heading matches the associated view.

tsx
<Routes>
  <Route path= element={<HomeView />} />
  <Route path= element={<GuidesView />} />
  <Route path= element={<AboutView />} />
</Routes>

Step 2 of 8

Keep a Shared Layout Around Changing Views

Restructure the static routes beneath a persistent shared layout with an index child and an Outlet.

See the nested route structure

One stable shell, changing views

Nested routes separate the stable parts of the interface from the view that changes with the URL.

Make the route at / render AppLayout. Its child route supplies the selected view through Outlet, which sits inside the shared main element. The header and the main landmark therefore stay mounted while a child view changes.

An index child has no path segment. It supplies HomeView at the parent URL, /. A relative child path such as "guides" combines with the parent path / to form /guides.

Route tree and rendered interface

A route-tree diagram showing a parent slash route rendering a stable header and main area, with index, guides, and about children rendering different views inside the main area.

The parent route controls the persistent shell. Its matching child controls the view inserted into the main content slot.

Build the persistent layout

Create the shared layout

Create src/AppLayout.tsx. This component owns the shared header and the application’s only main element. Outlet is the location where the matching child view is rendered. The header uses a paragraph for the application name, so each child view can remain responsible for its single h1.

Shared layout source

src/AppLayout.tsx

tsx
import { Outlet } from "react-router";

export function AppLayout() {
  return (
    <>
      <header className="site-header">
        <p className="site-name">Field Notes</p>
      </header>
      <main>
        <Outlet />
      </main>
    </>
  );
}

Nested route declarations

src/App.tsx

tsx
import { Route, Routes } from "react-router";
import { AppLayout } from "./AppLayout";
import { AboutView, GuidesView, HomeView } from "./views";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<AppLayout />}>
        <Route index element={<HomeView />} />
        <Route path="guides" element={<GuidesView />} />
        <Route path="about" element={<AboutView />} />
      </Route>
    </Routes>
  );
}

Tip

Check the restructure

Keep src/main.tsx and the three view components from the previous step. Save these two files, then directly enter /, /guides, and /about in your local browser. Each URL should select its view while the Field Notes header remains present.

Place the two route-tree pieces

Complete the shared-layout fragment

Complete this AppLayout return fragment so a matching child can render inside the shared main content region.

tsx
return (
  <>
    <header className="site-header">
      <p className="site-name">Field Notes</p>
    </header>
    <main>
      
    </main>
  </>
);

Complete the parent-URL child

Complete this route fragment so the parent URL receives its child view without an additional path segment.

tsx
<Routes>
  <Route path="/" element={<AppLayout />}>
    
    <Route path="guides" element={<GuidesView />} />
    <Route path="about" element={<AboutView />} />
  </Route>
</Routes>

Connect structure to behavior

Map the route tree

Match each route-tree element to the part of the rendered interface or URL behavior it controls.

Tap an item and then its matching pair.

Step 3 of 8

Navigate with Real Links

Add persistent Link-based primary navigation and verify client-side history alongside native browser link actions.

Use anchors for application destinations

Links belong in the shared layout

Your shared layout stays mounted while its child view changes. Put the primary navigation in that shared layout so it is available on Home, Guides, About, and the fallback view.

Link renders a real anchor element. For an ordinary same-tab activation, React Router handles the destination client-side and renders the matching view without loading a separate document. Put the application destination in to; do not replace navigation links with button handlers or direct location assignments.

Because the result is an anchor, browser link actions still matter: users can modifier-click, use the context menu, or open a destination in another tab.

One link, two browser behaviors

A diagram showing a persistent application header, a same-tab view change from a normal link activation, and a separate tab opened by a modifier-click.

An ordinary activation changes the routed view in the current tab. Modified link actions remain browser-controlled, such as opening a destination in another tab.

Add persistent primary navigation

src/AppLayout.tsx

Replace src/AppLayout.tsx with this shared layout. The nav remains outside Outlet, so it persists while child views render inside main.

tsx
import { Link, Outlet } from "react-router";

export function AppLayout() {
  return (
    <>
      <header className="site-header">
        <p className="site-name">Field Notes</p>
        <nav aria-label="Primary navigation">
          <Link to="/">Home</Link>
          <Link to="/guides">Guides</Link>
          <Link to="/about">About</Link>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </>
  );
}

What the three links preserve

Each to value is an absolute application destination. When an ordinary activation moves from Home to Guides, the browser receives a history entry. Its Back and Forward controls can therefore revisit the previous and next routed views.

At this stage, these are plain Link elements. Route-aware current-link styling comes later.

Set destinations with to

Practice: complete the destinations

Complete the to values for the three navigation links.

tsx
<nav aria-label="Primary navigation">
  <Link to="">Home</Link>
  <Link to="">Guides</Link>
  <Link to="">About</Link>
</nav>

Run the local navigation check

Verify history and native link actions

In your local app, make sure src/AppLayout.tsx contains the three Link destinations. Then perform this navigation journey:

  1. Start on Home and ordinarily activate Guides in the current tab.
  2. Use the browser Back control once, then Forward once.
  3. From the original tab, Ctrl-click or Cmd-click About, or use the link context menu to open it in a new tab.

Report the three Link destinations you used, the heading and URL sequence in the original tab, the heading and URL in the new tab, and whether the shared header and primary navigation remained available during the original-tab journey.

Write at least 120 characters (0/120).

Step 4 of 8

Recover from an Unmatched URL

Add an in-app fallback for unsupported URLs while preserving the shared layout and a route back to Home.

Keep the layout available

Catch unmatched paths inside the app

A wildcard child route catches a URL only when none of the supported child routes match. Its path="*" renders NotFoundView inside the existing AppLayout, so the shared header, navigation, and main landmark remain available.

This is an in-app fallback view—not a server-generated HTTP 404 response. It is React’s response after the application has loaded and examined the current URL.

How the fallback fits the route tree

Diagram showing supported and unmatched URLs rendering different content inside the same persistent application shell.

A wildcard child selects fallback content while the shared layout remains mounted.

Add a recovery view

src/App.tsx

Replace src/App.tsx with this route declaration. The wildcard is a child of the shared-layout route, just like the supported views.

tsx
import { Route, Routes } from "react-router";
import { AppLayout } from "./AppLayout";
import {
  AboutView,
  GuidesView,
  HomeView,
  NotFoundView,
} from "./views";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<AppLayout />}>
        <Route index element={<HomeView />} />
        <Route path="guides" element={<GuidesView />} />
        <Route path="about" element={<AboutView />} />
        <Route path="*" element={<NotFoundView />} />
      </Route>
    </Routes>
  );
}

src/views.tsx additions

Add the import and NotFoundView export to src/views.tsx. The view has its one route heading and a real recovery link to the Home destination.

tsx
import { Link } from "react-router";

export function NotFoundView() {
  return (
    <section>
      <h1>Page not found</h1>
      <p>The view at this address is not available.</p>
      <Link to="/">Home</Link>
    </section>
  );
}

Understand the loading boundary

Loading comes before route matching

A direct visit or browser refresh begins with a request to the development server. For this local exercise, Vite returns the application document for application paths. Then BrowserRouter reads the pathname and React can render either a supported view or the wildcard fallback.

The wildcard cannot help if the application document never loads. If a server sends its own error response instead, React has no running application in which to render NotFoundView. This distinction matters, but server hosting configuration is outside this tutorial.

Warning

Important distinction

Do not treat NotFoundView as proof of an HTTP status code. It is a client-side view selected after the React application is running.

Complete the fallback route

Complete the path value for the fallback child route.

tsx
<Route path="" element={<NotFoundView />} />

Run the local recovery check

Verify recovery from an unmatched URL

In your local field-notes project, add the wildcard child route and the recovery view. Then type /missing directly in the browser address bar and load it. Confirm that the shared navigation is still available, activate the Home recovery link, and report the heading and destination you observed at each point.

Write at least 80 characters (0/80).

Step 5 of 8

Make the Current View Clear

Use NavLink to show the current destination with aria-current while keeping keyboard focus visibly separate.

Separate location from focus

Two navigation states, two questions

Persistent navigation needs two independent signals:

  • A current link answers: “Where am I?”
  • Keyboard focus answers: “What will Enter activate?”

NavLink is React Router’s route-aware link component. It renders a real anchor and retains ordinary link behavior, while React Router detects whether its destination matches the current URL. A matching NavLink automatically receives aria-current="page"; do not calculate or add that attribute yourself.

Use end on Home to express that Home is current only at the root destination. React Router also gives the root destination special handling, so / is not falsely current at /guides, but end makes the intended matching policy clear.

At /missing, none of the three known navigation destinations matches. No link should be marked current.

A navigation bar where the first item has a current-location treatment and a different third item has a keyboard-focus outline.

The current-link treatment and keyboard-focus treatment remain distinct even when they appear on different links.

Read the signals separately

These states can coincide, but they describe different facts.

Current location and keyboard focus are independent interface states.
StateMeaningRecommended signal
Current linkIts destination matches the URLaria-current="page" plus a persistent visual treatment
Focused linkIt receives keyboard inputA :focus-visible outline
Both statesThe current link also has keyboard focusBoth treatments are visible

Add route-aware navigation

Use NavLink in the shared layout

In src/AppLayout.tsx, replace the primary navigation’s Link elements with NavLink elements. Keep the shared header, navigation region, and main landmark in this layout; child views still render through Outlet. The links use absolute application destinations.

Shared layout with route-aware links

src/AppLayout.tsx

tsx
import { NavLink, Outlet } from "react-router";

export function AppLayout() {
  return (
    <div className="app-shell">
      <header>
        <p className="app-name">Field Notes</p>
        <nav className="primary-nav" aria-label="Primary navigation">
          <NavLink end to="/">Home</NavLink>
          <NavLink to="/guides">Guides</NavLink>
          <NavLink to="/about">About</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

Keep the visual indicators distinct

Add these rules to src/styles.css. The filled current-link treatment answers a location question; the high-contrast outline answers a keyboard-focus question.

css
.primary-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.primary-nav a {
  border-bottom: 3px solid transparent;
  border-radius: 0.25rem;
  color: #173a5e;
  padding: 0.5rem 0.75rem;
  text-decoration: none;
}

.primary-nav a[aria-current="page"] {
  background: #d9ecff;
  border-bottom-color: #005a9c;
  color: #003b68;
  font-weight: 700;
}

.primary-nav a:focus-visible {
  outline: 3px solid #b54708;
  outline-offset: 3px;
}

Practice: complete both signals

Complete the current and focused link states

Complete the NavLink markup and CSS. Home must use endpoint matching. One rule styles the destination that matches the URL; the other styles the link receiving keyboard focus.

tsx
<  to="/">Home</NavLink>

.primary-nav a[="page"] {
  background: #d9ecff;
  border-bottom-color: #005a9c;
}

.primary-nav a: {
  outline: 3px solid #b54708;
  outline-offset: 3px;
}

Step 6 of 8

Synchronize the Browser-Tab Title

Synchronize meaningful browser-tab titles with the current React Router pathname, including canonical static paths and unmatched URLs.

Observe the Current Pathname

Connect the URL to the browser tab

React Router renders the matching view, but routing does not automatically choose a useful document.title. In the persistent shared layout, useLocation() exposes the current location. Read its pathname, look up a static heading label, and synchronize the browser-managed title with an Effect.

The Effect runs on the initial application render and whenever the pathname changes. Each result follows one consistent pattern: Heading | Field Notes. This helps people distinguish Field Notes views in browser tabs, history, and task switchers.

Title synchronization flow

A diagram showing a browser pathname flowing through normalization and a label lookup to update a browser-tab title.

The shared layout observes the pathname and derives one consistent title for the current view.

Look Up a Stable Heading

Use canonical paths

Keep the supported labels in pageHeadings. Before reading that object, convert the pathname to lowercase and remove trailing slashes. The empty result for the root path becomes /.

A direct entry such as /GUIDES/ therefore receives the same label as /guides. A pathname that is not a supported key uses Page not found, matching the fallback view.

Canonical title results

The canonical pathname is the key used to select the static heading label.

Static views and unmatched paths use predictable titles.
Incoming pathnameCanonical pathnameResulting title
//Home | Field Notes
/guides/guidesGuides | Field Notes
/GUIDES//guidesGuides | Field Notes
/missing//missingPage not found | Field Notes

Synchronize document.title

src/AppLayout.tsx

Replace src/AppLayout.tsx with this version. It preserves the shared layout and the primary-nav class used by the existing current-link and keyboard-focus styles. The Effect is the new browser synchronization.

tsx
import { useEffect } from "react";
import { NavLink, Outlet, useLocation } from "react-router";

const pageHeadings: Record<string, string> = {
  "/": "Home",
  "/guides": "Guides",
  "/about": "About",
};

function getPageHeading(pathname: string): string {
  const canonicalPathname = pathname.toLowerCase().replace(/\/+$/, "") || "/";

  return pageHeadings[canonicalPathname] ?? "Page not found";
}

export function AppLayout() {
  const { pathname } = useLocation();

  useEffect(() => {
    const pageHeading = getPageHeading(pathname);
    document.title = `${pageHeading} | Field Notes`;
  }, [pathname]);

  return (
    <div className="app-shell">
      <header>
        <p className="app-name">Field Notes</p>
        <nav className="primary-nav" aria-label="Primary navigation">
          <NavLink end to="/">Home</NavLink>
          <NavLink to="/guides">Guides</NavLink>
          <NavLink to="/about">About</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

Practice: Update and Check Titles

Complete and verify the title Effect

Complete the location-driven title Effect. Then save it in AppLayout and verify it after direct entry, link navigation, and browser Back or Forward.

tsx
// Inside AppLayout, after getPageHeading is available:
const { pathname } = ();

useEffect(() => {
  const pageHeading = getPageHeading(pathname);
  document.title = `${pageHeading} | `;
}, []);

Step 7 of 8

Move Focus Deliberately After Navigation

Add a route-change focus policy that orients keyboard users without overriding initial browser focus.

Orient keyboard users after a view change

Why route changes need a focus policy

A client-side view change updates the content inside the persistent shared layout, but it does not inherently move keyboard focus into that content. After activating a navigation link, focus can remain on that link in the header; after browser history navigation, the previously focused element might no longer exist.

Use a deliberate page-level policy: after the pathname changes, focus the newly committed route heading. This gives keyboard users a clear starting point in the new view.

Do not move focus on the initial document load. The browser manages initial focus, and the application should not override it merely because it rendered for the first time.

Focus moves to the new route heading

Diagram of a persistent header and changing main content, with a visible focus outline around the destination view heading.

The shared layout stays mounted; the focus target is the route heading inside its main landmark.

Create a focusable, visible target

Prepare each route heading

Make every route heading programmatically focusable. tabIndex={-1} allows code to focus the heading, but it does not add the heading to the normal Tab sequence.

In src/views.tsx, update the one h1 in each view. Keep the exact heading labels used by the title lookup.

Route heading updates

In src/views.tsx, use these route-heading elements in the existing view components.

tsx
<h1 tabIndex={-1}>Home</h1>

<h1 tabIndex={-1}>Guides</h1>

<h1 tabIndex={-1}>About</h1>

<h1 tabIndex={-1}>Page not found</h1>

Visible heading focus

Add this rule to src/styles.css. Keep the existing [aria-current="page"] styling and link :focus-visible styling: those communicate different states.

css
h1:focus {
  outline: 3px solid #155eef;
  outline-offset: 4px;
}

Tip

Keep the signals separate

A current link says, “this is the current view.” A focus indicator says, “keyboard input is here.” Keep both indicators visible and visually distinct.

Locate and focus the committed heading

Focus only after a pathname change

In the persistent AppLayout, retain a ref for the one shared main element and another ref for the previous pathname. An Effect runs after React has committed the destination view, so the heading can be found safely inside main.

Initialize previousPathnameRef with the current pathname. If the pathname is unchanged, return without moving focus. This skips the initial load and also prevents a development StrictMode Effect replay from focusing again. Do not key the shared layout by pathname.

Shared layout focus Effect

In src/AppLayout.tsx, keep the title Effect from the previous step and make these additions. The existing header and primary navigation remain unchanged.

tsx
import { useEffect, useRef } from "react";
import { NavLink, Outlet, useLocation } from "react-router";

// Inside AppLayout, after reading pathname with useLocation():
const mainRef = useRef<HTMLElement>(null);
const previousPathnameRef = useRef(pathname);

useEffect(() => {
  if (previousPathnameRef.current === pathname) {
    return;
  }

  previousPathnameRef.current = pathname;
  mainRef.current?.querySelector<HTMLHeadingElement>("h1")?.focus();
}, [pathname]);

// In the existing returned shared layout:
<main ref={mainRef}>
  <Outlet />
</main>

Tip

What to check locally

Test the policy after saving:

  1. Enter /guides directly in a fresh browser tab. The application updates the title, but this code does not programmatically focus its heading.
  2. Use a primary-navigation link. The destination route heading receives focus and its outline appears.
  3. Use Back and Forward. Each pathname-changing history navigation focuses the committed route heading, including the fallback view.

This policy does not attempt to restore the exact element that had focus earlier in history.

Apply the pathname guard

Complete the focus policy

Complete the existing pathname-change Effect.

tsx
useEffect(() => {
  if (previousPathnameRef.current === ) {
    return;
  }

  previousPathnameRef.current = pathname;
  mainRef.current?.querySelector<HTMLHeadingElement>("h1")?.();
}, [pathname]);

Check the complete focus journey

Verify direct entry and history navigation

Run the finished application and inspect the focus behavior rather than predicting it.

Start with a fresh direct entry to /guides. Then activate the About primary-navigation link, use Back, and use Forward. Report whether the route-heading focus outline appeared after the fresh load and identify the focused route heading after each later navigation.

Write at least 120 characters (0/120).

Step 8 of 8

Check the Complete Navigation Experience

Validate the completed Field Notes routes through direct URL entry, keyboard navigation, browser history, reloads, native link actions, and unmatched-path recovery.

Know What to Observe

Set Up the Observation Criteria

Run this check in your local Field Notes application with the development server running. It is a repeatable browser journey, not an automated inspection.

For every stop, observe five things:

  1. The rendered route heading.
  2. The persistent shared layout: header, primary navigation, and main landmark.
  3. The current link indicator—or the absence of one on an unmatched view.
  4. The browser-tab title.
  5. The element that visibly receives keyboard focus after navigation.

Use a fresh tab for initial URL entries. A fresh load must render the correct view and title, but the application must not impose focus on its route heading. After a pathname-changing in-app navigation, Back, or Forward action, the route-heading focus policy applies.

Two Navigation Situations

Diagram comparing a fresh URL load with link-based navigation: only the link-based route change visibly focuses the new heading, while the current navigation link has a separate visual state.

Check location, title, current-link state, and focus as separate signals. A current link is not the same thing as a focused link.

Run the Browser Journey

Local Navigation Journey

Work through this sequence. Replace “local origin” with the address shown by your own Vite development server. Keep the browser’s tab title visible while checking each view.

Expected observations for the finished application
ActionRendered resultTitle and navigationFocus expectation
In fresh tabs, directly enter /, /guides, and /aboutMatching Home, Guides, or About route heading; shared layout remains presentMatching heading followed by | Field Notes; matching current linkNo application-imposed route-heading focus
Starting at Home, Tab to Guides and press EnterGuides route heading; shared layout persistsGuides is the current link; Guides | Field NotesGuides route heading receives focus
Use browser BackHome route heading; shared layout persistsHome is the current link; Home | Field NotesHome route heading receives focus
Use browser ForwardGuides route heading; shared layout persistsGuides is the current link; Guides | Field NotesGuides route heading receives focus
Reload while on /guidesGuides route heading; shared layout persistsGuides is current; Guides | Field NotesNo application-imposed route-heading focus
Open a known navigation link in another tabDestination loads in the new tab as a normal link destinationNew tab has its matching title and current linkNew document load: no application-imposed heading focus
Directly enter /missing, then activate its Home recovery linkPage not found, then Home; shared layout persistsNo current link on fallback; fallback title is Page not found | Field Notes; then Home titleFresh fallback load does not impose focus; recovery navigation focuses Home heading

Tip

Keep the Indicators Distinct

When using the keyboard, verify that the focused navigation link has its keyboard focus indicator before activation. After the view changes, the route heading should have its own focus style. These indicators have different jobs: the first identifies keyboard input; the second confirms the newly reached content.

Apply the Complete Checklist

Record and Diagnose Your Results

Run the local navigation journey before answering.

Write a short verification record in 4–7 bullets. Report observations for: a fresh direct entry, keyboard activation followed by Back and Forward, reload, opening a navigation link in another tab, and /missing recovery. For each relevant stop, include the route heading, title, current-link state, persistent shared layout, and focus result.

Then identify any mismatch and state the concrete correction required by the checklist. If every observation matches, explicitly say that no correction is required. Finally, explain why a fresh entry or reload and a pathname-changing in-app navigation intentionally have different focus behavior.

Write at least 450 characters (0/450).

Finish

Summary

What You Validated

  • Nested routes keep the shared layout available while the Outlet changes views.
  • Links and NavLinks preserve meaningful anchor behavior, history navigation, and current-link semantics.
  • Location-based titles identify the current view in the browser tab, including the unmatched fallback.
  • Fresh document loads render the correct view and title without an application-imposed focus move.
  • After a pathname change through a Link, Back, Forward, or recovery link, deliberate focus on the route heading provides orientation.

Tutorial Complete

Congratulations! You completed: Build Accessible Client-Side Routes with React Router

You completed an accessible client-side routing journey: URL-addressable views, a persistent shared layout, native link behavior, current-link status, synchronized titles, and deliberate route-heading focus.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate