Topic 7.3
Keyboard and Focus
In one line
Every control must work with the keyboard alone: interactive elements are focusable in source order, tabindex 0 and -1 adjust that, a skip link jumps past repeated menus, and focus must always be visible.
Think of it like this
A TV remote with only arrow and OK buttons. A highlight box moves from tile to tile, and you always need to see where it is. If the box disappears, or some tiles can't be reached, the remote is useless. Keyboard users browse the web the same way: the highlight is the focus.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Focus
- The marker for the one element that receives key presses right now.
- Focusable
- Able to receive focus, with Tab or with JavaScript's
focus(). - Tab order
- The order focus moves through elements when you press Tab. By default, the order they appear in the HTML.
- tabindex
- An attribute that makes an element focusable (
0), focusable only by script (-1), or (badly) reorders focus (positive numbers). - Skip link
- A link at the very top that jumps to the main content, so keyboard users can skip the menu.
- Focus ring
- The outline the browser draws around the focused element.
- :focus-visible
- A CSS selector that matches focused elements when the browser decides a ring should show, mainly for keyboard focus.
- Keyboard trap
- A place where focus gets stuck and Tab can't move out. WCAG 2.1.2 forbids it.
- document.activeElement
- The JavaScript property that holds the element that has focus right now.
Step by step
01Press Tab on a plain page
Focus starts on the page itself. Each Tab press moves to the next focusable element in source order. Links, buttons and fields are in the list; paragraphs and headings are not (screen reader users move through those with other keys, like H for headings).
02Add a skip link
Put the link first in <body> and give <main> the matching id. Following an in-page link moves focus to the target (or to its position, so the next Tab continues from there).
<body>
<a class="skip" href="#main">Skip to main content</a>
<header>
<nav><!-- 20 menu links --></nav>
</header>
<main id="main">
<h1>Course catalogue</h1>
</main>
</body>03Hide it until it gets focus
Moving it off-screen keeps it in the Tab order; :focus brings it back into view. Don't use display: none or visibility: hidden: those make it unfocusable.
.skip {
position: absolute;
left: -9999px;
}
.skip:focus {
left: 8px;
top: 8px;
padding: 8px 12px;
background: #fff;
z-index: 10;
}04Keep the focus ring, make it better
If the design team dislikes the default ring on mouse clicks, style :focus-visible instead of deleting outlines. A 2–3 px solid outline with a small offset, in a colour with 3:1 contrast against the background, works on almost any design.
:focus-visible {
outline: 3px solid #1a56db;
outline-offset: 2px;
}
/* never this on its own: */
/* *:focus { outline: none; } */05Move focus on purpose with tabindex="-1"
After an action that changes a large part of the page (loading search results, moving to the next step of a form), move focus to the new heading so keyboard and screen reader users start there. tabindex="-1" lets the heading take focus from script without adding it to the Tab order.
<h2 id="results-title" tabindex="-1">12 results for "flexbox"</h2>
<script>
document.getElementById("results-title").focus();
</script>06Test it
Unplug the mouse (or don't touch it) and Tab through the page from top to bottom. Check: can you reach every control, is the order logical, can you always see the ring, can you operate everything with Enter, Space, arrows and Esc, and can you always Tab out again?
Try it yourself
- 1
Predict what can take focus
In the first example, add
<details><summary id="sum">More</summary>Hidden text</details>,<textarea id="ta"></textarea>and<span id="edit" contenteditable>Edit me</span>. Predict which are focusable, then run it. (All three are.) - 2
Break the visual order
In the second example's CSS, add
nav { display: flex; flex-direction: row-reverse; justify-content: flex-end; }. Click in the preview and Tab through the menu: the links appear right-to-left visually but focus still goes in source order. This mismatch is a WCAG 2.4.3 problem. - 3
Remove the ring and feel the pain
Replace the
:focus-visiblerule with* { outline: none; }, click in the preview and Tab around. Try to tell where you are. Then put it back.
Code & diagrams
Surprise: the `tabIndex` property says 0 for every `a` and `button`, even a link without href and a disabled button, because the spec gives those elements a default of 0. Calling `focus()` and checking `document.activeElement` is the real test. The heading is focusable by script but skipped by Tab. Click in the preview and press Tab to walk the real order.
Preview
Console
Nothing logged yet.Expected console output
link | focusable: true | tabIndex: 0
nohref | focusable: false | tabIndex: 0
btn | focusable: true | tabIndex: 0
off | focusable: false | tabIndex: 0
field | focusable: true | tabIndex: 0
plain | focusable: false | tabIndex: -1
zero | focusable: true | tabIndex: 0
heading | focusable: true | tabIndex: -1Removing the outline on `main` is fine because it's a landing spot, not something you operate. Never remove it from links, buttons or fields.
Preview
Console
Nothing logged yet.Expected console output
focus -> Skip to main content
focus -> mainBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Delete every focus outline
Add *:focus { outline: none; } to the stylesheet (a common "reset" line).
Break #2
Use a positive tabindex
Give the newsletter field tabindex="1" so it gets focus first.
Break #3
Hide the skip link with display: none
Write .skip { display: none; } and .skip:focus { display: block; }.
Myth vs fact
Myth
Screen reader users press Tab to read the page.
Fact
Tab only visits interactive elements. Screen reader users read with their screen reader's own keys: arrows line by line, H for headings, D (NVDA) for landmarks, K for links.
Myth
Adding tabindex="0" makes a div accessible.
Fact
It only makes the div focusable. It still has no role, no name and no Enter or Space behaviour. Use a real <button> or <a>.
Myth
Focus outlines are ugly, so designers are right to remove them.
Fact
Removing them breaks the page for keyboard users. :focus-visible lets you show a well-designed ring only for keyboard focus.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
:focus-visibleuses browser heuristics: it matches after keyboard navigation, always for text inputs (because typing follows), and usually not after a mouse click on a button. When script callsfocus(), browsers generally show the ring if the last interaction was from the keyboard. Firefox, Chrome and Safari have all supported it since 2022. - ▸
The **
inert** attribute (supported in all major browsers since 2023) removes a whole subtree from focus, clicks and the accessibility tree.dialog.showModal()makes everything outside the dialog inert automatically and returns focus to the opener when it closes; build modals on<dialog>rather than writing your own focus trap. - ▸
Roving tabindex is the pattern for composite widgets (tabs, toolbars, menus, grids): only one item has
tabindex="0", the others-1; arrow keys move focus and swap the values. The whole widget is one Tab stop, which matches how native radio groups behave. The ARIA Authoring Practices Guide (APG) documents the keys for each widget. - ▸
WCAG 2.2 added 2.4.11 Focus Not Obscured (Minimum) (AA): the focused element must not be entirely hidden by author content such as a sticky header or cookie banner. CSS
scroll-padding-topon the scroll container keeps focused items from sliding under a fixed header.
Remember this
- 1
Focus is the browser's marker for which element receives keyboard input right now. Only one element has focus at a time; JavaScript can read it as
document.activeElement. Many people navigate by keyboard: blind screen reader users, people with motor disabilities, switch and voice-control users (their tools send key presses), and power users. WCAG 2.1.1 Keyboard (A) says all functionality must work from a keyboard. - 2
The keys:
Tabmoves focus forward,Shift+Tabbackward.Enterfollows a link and presses a button.Spacepresses a button and toggles a checkbox. Arrow keys move inside a group: between radio buttons with the samename, through<select>options, in sliders.Esccloses dialogs and popovers. Native elements do all of this for free; a custom widget has to rebuild it with JavaScript. - 3
Focusable elements by default: links with an
href,<button>,<input>(excepttype="hidden"),<select>,<textarea>,<summary>,<iframe>,<audio>and<video>withcontrols, and anything withcontenteditable. Disabled form controls are not focusable. A link withouthrefand plaindiv/spanare not focusable. - 4
Tab order is the order elements appear in the HTML source (the DOM order), not where CSS places them on screen. If CSS (
order,flex-direction: row-reverse, grid placement,position: absolute) moves things around, focus can jump in a confusing zigzag: WCAG 2.4.3 Focus Order (A). Fix the source order rather than fighting it. - 5
The
tabindexattribute changes focus behaviour.tabindex="0"makes an element focusable and puts it in the Tab order at its source position (use it only on custom widgets that also have a role and keyboard handling).tabindex="-1"makes an element focusable by JavaScript (el.focus()) or by a link to itsid, but not with Tab: perfect for headings or containers you move focus to. Positive values (tabindex="1"and up) jump ahead of everything else and create an order that's almost impossible to maintain: never use them. - 6
A skip link is the first link on the page,
<a href="#main">Skip to main content</a>, pointing at the<main id="main">element. Without it, keyboard users press Tab through the whole menu on every page (WCAG 2.4.1 Bypass Blocks, A). It can be hidden until focused with CSS, but must appear on screen when it gets focus. And focus must be visible: the browser draws a focus ring; never remove it withoutline: nonewithout a replacement (WCAG 2.4.7 Focus Visible, AA). The CSS:focus-visiblepseudo-class shows a ring for keyboard focus without showing it on every mouse click.
Explain it without notes
Which elements are focusable by default, and how does tabindex change that?
What is a skip link, why is it needed, and how do you hide it without breaking it?
Why is outline: none dangerous, and what should you do instead?
What is a keyboard trap? Give an example and how to avoid it.
Practice
Write a page header with a skip link, a nav of three links and a main with an h1, plus the CSS that hides the skip link until it's focused.
Write CSS that shows a 3 px blue outline with a 2 px offset for keyboard focus on every element, without showing it after mouse clicks on buttons.
A product card is <div class="card" onclick="openProduct()">. Rewrite it so it works with the keyboard without adding JavaScript key handlers.
Trade-offs
- ↔
Moving focus after an action helps keyboard and screen reader users find new content, but moving it too often is disorienting; move it only when the user's context really changes (new page section, dialog opened or closed, error summary).
- ↔
Hiding the skip link until focus keeps the design clean but sighted mouse users never learn it exists; some sites show it always, which is also fine.
Done when you can
I can list the natively focusable elements and test focus with
focus()anddocument.activeElement.I use
tabindex="0"and"-1"correctly and never positive values.I can add a skip link that appears on focus.
I keep focus visible and style it with
:focus-visible.I can test a page with Tab, Shift+Tab, Enter, Space, arrows and Esc.
I keep the DOM order the same as the visual order.