Keyboard, Focus & Skip Links
Every user interaction available by mouse must also be reachable and operable by keyboard alone. That requires understanding the tab order, keeping the focus ring visible, and providing a skip link so keyboard users can bypass repeated navigation on every page load.
What keyboard accessibility means
Tab order is the sequence in which the browser moves focus from element to element when a user presses Tab. By default it follows DOM order and only includes natively interactive elements: <a> with an href, <button>, <input>, <select>, <textarea>, and <details>. Non-interactive elements like <div> and <p> are skipped entirely.
The tabindex attribute controls whether an element participates in the tab order. tabindex="0" adds a custom element (like a div acting as a widget host) to the natural tab order at its DOM position. tabindex="-1" removes an element from the tab order while still allowing programmatic focus via JavaScript's .focus() method, which is useful for moving focus to a modal or announcement when it opens. A positive tabindex (1, 2, 3...) creates a separate priority queue that fires before the natural DOM order, which almost always produces a confusing sequence and should be avoided.
Why it matters
WCAG 2.1.1 (Keyboard, Level A) requires that all functionality is available via keyboard alone. This is not optional. Users with motor disabilities, power users, and anyone on a keyboard-only device depend on it. A page that hides a feature behind a hover effect, or puts critical actions in a <div> without tabindex, silently excludes those users.
WCAG 2.4.7 (Focus Visible, Level AA) requires that the focused element is always visually identifiable. The single worst practice in web development is *:focus { outline: none } with no replacement. It removes the only visual cue keyboard users have for their position on the page. WCAG 2.4.11 (Focus Appearance, Level AA, added in WCAG 2.2) further requires the focus indicator to have sufficient size and 3:1 contrast ratio against adjacent colors.
How to implement it
For focus styling, use :focus-visible instead of :focus. The :focus pseudo-class fires on keyboard navigation AND mouse clicks. :focus-visible fires only when the browser determines the user is navigating by keyboard or a programmatic .focus() call. This lets you show a clear ring for keyboard users while suppressing it for mouse users who find the ring visually noisy. The pattern: button:focus-visible { outline: 2px solid #color; outline-offset: 2px; } with button:focus:not(:focus-visible) { outline: none; }.
For skip links, add <a href="#main-content" class="skip-link">Skip to main content</a> as the absolute first child of <body>. Give <main id="main-content"> the matching id. Style it visually hidden by default and visible on focus: .skip-link { position: absolute; left: -9999px; } .skip-link:focus { left: 0; top: 0; }. This satisfies WCAG 2.4.1 (Bypass Blocks) for keyboard users who do not use a screen reader's landmark shortcuts.
Use tabindex="-1" on dynamic content containers so JavaScript can move focus there programmatically when the content appears. After a route change in a single-page app, call .focus() on the new page's <h1> (with tabindex="-1" on it) so AT users know new content has loaded. After closing a modal, return focus to the button that opened it, so the user's position in the page is preserved.
Try it
Step through the tab order on a page wireframe and see the focus ring walk each element, then watch the skip link appear when it receives focus.
Press "Next Tab" to walk focus through the page in DOM order.
Check yourself
Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.
Remember this
- Tab order follows DOM order. Only natively interactive elements are included by default.
tabindex="0"adds an element to the natural tab order;tabindex="-1"removes it but allows.focus(); positive values are an anti-pattern.- Never remove
outlinewithout providing a:focus-visiblereplacement that meets WCAG 2.4.7 and 2.4.11. - The skip link must be the first focusable element in
<body>and itshrefmust match theidon<main>.
Done with this concept?
Mark it complete to track your progress. No login needed.