What is a skip link and why does it matter? How do you handle focus when a client-side route changes?
Bypassing repeated content and the SPA-specific problem of route changes not moving focus.
A SKIP LINK is a link, first in the tab order, that jumps past repeated blocks (nav, header) straight to <main>. Keyboard and screen-reader users otherwise Tab through the same 30 nav links on every page. Convention: it's visually hidden until focused, then appears; it targets an id on the main region (href="#main"), which should have tabindex="-1" so focus actually lands there in all browsers. This satisfies WCAG 2.4.1 (Bypass Blocks). The SPA problem is subtler and widely broken: with a real page navigation the browser resets focus to the top and screen readers announce the new page. With CLIENT-SIDE routing, the URL and DOM change but focus stays on the link the user clicked and NOTHING is announced — a screen-reader user has no idea the page changed, and a keyboard user's next Tab resumes from a stale location. The fix on each route change: move focus to a sensible target — typically the new page's <h1> (given tabindex="-1") or the main container — and/or announce the new page title via a polite live region (a visually-hidden aria-live region updated with the route name). Also update document.title so the tab/announcement reflects the page. Frameworks don't do this automatically; it's the app's responsibility. The senior framing: an SPA must recreate the two things a full navigation gave for free — focus reset and a page-change announcement.
Adding a skip link; fixing SPA route changes that leave focus stranded and unannounced.
// Skip link (first focusable element on the page)
// <a class="skip-link" href="#main">Skip to content</a>
// <main id="main" tabindex="-1">…</main>
// On every client-side route change, move focus + announce:
function onRouteChange(pageTitle) {
document.title = pageTitle; // reflect the new page
const h1 = document.querySelector('main h1');
if (h1) { h1.setAttribute('tabindex', '-1'); h1.focus(); } // focus lands on new content
announce(pageTitle); // update a polite aria-live region
}
// Visually-hidden live region kept in the DOM:
// <div aria-live="polite" class="sr-only" id="route-status"></div>
function announce(msg){ document.getElementById('route-status').textContent = msg; }