Keyboard Accessibility
A mouse is just one way to use a computer. Many people never touch one, or cannot. People who are blind use a keyboard (or touch gestures) with a screen reader. People with tremors, paralysis, or repetitive strain injuries may use a keyboard, a switch, a mouth stick, or voice commands that behave like a keyboard. Many power users simply find the keyboard faster.
That is why keyboard accessibility is one of the most important rules in WCAG (2.1.1), and one of the easiest to test. If everything works with the keyboard, it will usually work with many other assistive tools too. And if it does not, it is broken for a huge group of people.
What You Will Learnβ
- The keys people use to browse
- Which elements are focusable by default
- How
tabindexworks, and which values to use and avoid - Keeping a logical focus order
- Making focus clearly visible, with
:focus-visible - Avoiding keyboard traps
- How arrow keys and roving tabindex work in custom widgets
- How to test your own pages
The Big Ideaβ
- HTML Code
- Focus Order Lab
<!-- Everything here works with the keyboard, with no extra code -->
<a href="/recipes">Recipes</a>
<button type="button">Subscribe</button>
<label for="email">Email address</label>
<input id="email" type="email">
<input type="checkbox" id="news">
<label for="news">Send me the newsletter</label>
<select aria-label="Choose a loaf">
<option>Sourdough</option>
<option>Rye</option>
</select>
Click it (or press Enter), then press Tab repeatedly.
Focus log (most recent last):
- (press Tab after clicking Start here)
With no changes, focus follows the order in the code, which is also the visual order.
This is real browser behaviour, measured when each button gets focus. The exact starting point after clicking can differ a little between browsers.
Use the lab as a real test. Start with everything off, and Tab through. Then turn on each option, one at a time, and read the log.
The Keys That Matterβ
| Key | What it does |
|---|---|
| Tab | Moves focus to the next focusable element |
| Shift + Tab | Moves focus to the previous one |
| Enter | Follows a link, presses a button, submits a form |
| Space | Presses a button, ticks a checkbox, scrolls the page |
| Arrow keys | Move within a group: radio buttons, a select list, tabs, menus, sliders |
| Esc | Closes a dialog, menu, or pop-up |
| Home and End | Jump to the first or last item in a list or widget |
| Page Up and Page Down | Scroll, or move in large steps |
Real HTML elements already follow these rules. That is the biggest reason to use them.
What Can Receive Focus?β
Some elements are focusable by default. They are in the Tab order automatically:
| Element | Notes |
|---|---|
<a href="β¦"> | Only links with an href |
<button> | Unless disabled |
<input>, <select>, <textarea> | Unless disabled. type="hidden" is not focusable |
<summary> | The toggle of a <details> element |
<iframe> | The frame itself is focusable in many browsers |
[contenteditable] | Editable areas |
Elements with a tabindex | See below |
Not focusable by default: div, span, p, headings, images, and an a with no href. If you give one a click handler, the keyboard still cannot reach it. That is why the guidance is: use a button or a link.
Two details that surprise people:
- A disabled button or input is removed from the Tab order. Keyboard users cannot even find it, and screen readers may skip it. If you want users to discover why something is unavailable, consider
aria-disabled="true"with an explanation, and keep it focusable. You then have to block the action yourself. - Radio buttons in a group are one Tab stop. Tab goes into the group, and the arrow keys move between the choices.
tabindex: Use Only Three Kinds of Valueβ
| Value | Meaning | Use it for |
|---|---|---|
| (none) | The natural behaviour of the element | Almost always |
tabindex="0" | Put this element in the natural Tab order | Custom widgets that must be focusable, when no native element fits |
tabindex="-1" | Focusable by script or a link, but not reachable with Tab | Moving focus to a heading, an error summary, or a dialog with element.focus() |
tabindex="1" or higher | Jump the queue: visited before everything else | Never. It overrides the natural order for the whole page, and is nearly impossible to maintain |
<!-- Good: moves focus to the new content after a navigation -->
<h1 id="page-title" tabindex="-1">Checkout</h1>
<script>
document.getElementById("page-title").focus();
</script>
<!-- Bad: positive values -->
<button tabindex="3">Next</button>
It looks like a handy way to control order, but it creates a second, hidden order that everyone who edits the page later must understand. Fix the source order instead.
Focus Orderβ
The Tab order follows the order of the HTML. For a clear experience:
- Write your HTML in the order you want it used, and read from top to bottom, left to right (or right to left for RTL languages).
- Do not rearrange the screen with CSS in ways that disagree with the code. This includes
flex-direction: row-reverse, theorderproperty, grid placement, and absolute positioning of interactive controls. - When something appears, such as a dialog, a menu, or a validation summary, place it in the code where it will make sense, or move focus to it on purpose.
- When something disappears, put the focus somewhere sensible. If the focused element is removed, focus can be lost to the top of the page.
Turn on the reverse option in the lab above, and see what happens. The log will tell you the focus moved backwards on screen.
Seeing Where You Are: Focus Indicatorsβ
If a keyboard user cannot see the focus, they cannot use the page. WCAG 2.4.7 (Focus Visible) requires a visible focus indicator, and WCAG 2.2 also asks that focus is not hidden behind other content (2.4.11).
Browsers draw a default ring. The most common mistake is to remove it:
/* The most harmful line of CSS on the web */
*:focus {
outline: none;
}
Instead, style it, and make it clear:
:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
}
:focus-visibleapplies when the browser decides focus should be visible, which is mostly keyboard use, and for text fields. Many browsers do not show it when a button is clicked with a mouse.:focusapplies in every case, including mouse clicks.- Make the indicator at least 3 to 1 in contrast against its background, and thick enough to see (an outline of 2 or 3 pixels works well).
- Check dark and light areas. A ring that works on white may vanish on a dark header.
- Do not hide focus rings with
box-shadowtricks alone in forced-colour (high-contrast) modes, where shadows may not display. Anoutlinekeeps working. - Make sure sticky headers do not cover the focused element. Use
scroll-padding-topon the page so scrolled-to items are not hidden.
- HTML Code
- Focus Style Test
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
}
html {
scroll-padding-top: 5rem; /* the height of a sticky header */
}
Press Tab to move through the controls. Then click them with the mouse, and compare.
Every browser draws its own default ring. It is usually acceptable, but check it on your colours.
These are real focus styles on real elements, so what you see is what a visitor would see. Which clicks show a ring under :focus-visible depends a little on the browser and the element.
Keyboard Trapsβ
A keyboard trap is when focus gets stuck: a user can tab into something, but cannot tab or press a key to leave. It is a serious barrier, and it fails WCAG 2.1.2 (No Keyboard Trap).
Common causes:
- A custom widget or embedded player that captures keys and never releases them.
- A modal dialog with no way to close it from the keyboard.
- A third-party chat widget or advert in an iframe.
- A rich text editor that uses Tab for indentation, with no other exit.
A modal dialog is the one intentional trap: while it is open, focus should stay inside it, and Esc (and a close button) must always get out. The native <dialog> element, opened with showModal(), handles most of this for you. See Focus Management for the full pattern.
Keyboard Behaviour for Custom Widgetsβ
When you must build a custom widget, give it the keyboard behaviour that people already know from similar widgets. The WAI-ARIA Authoring Practices Guide documents patterns for tabs, menus, accordions, dialogs, listboxes, and more. The general ideas:
| Widget | Keys |
|---|---|
| Tabs | Tab enters the tab list. Arrow keys move between tabs. Tab moves on to the panel |
| Menu or menu bar | Arrow keys move between items. Enter or Space activates. Esc closes |
| Toolbar | Arrow keys move between buttons. One Tab stop for the whole toolbar |
| Accordion | Tab between headers. Enter or Space opens and closes |
| Dialog | Focus moves in. Tab cycles inside. Esc closes. Focus returns to the opener |
| Slider | Arrow keys change the value. Home and End jump to the ends |
| Listbox, select | Arrow keys move. Typing a letter jumps to a matching item |
Roving Tabindexβ
For groups such as toolbars and tab lists, the roving tabindex pattern keeps the Tab order short. Only the active item has tabindex="0". The others have tabindex="-1". Arrow keys move the 0 from one item to the next, and move focus with it.
Try it. Switch the pattern off and on, and compare the number of Tab presses you need to get past the toolbar.
- HTML Code
- Roving Toolbar
<div role="toolbar" aria-label="Text formatting">
<button type="button" tabindex="0">Bold</button>
<button type="button" tabindex="-1">Italic</button>
<button type="button" tabindex="-1">Underline</button>
</div>
<script>
// On ArrowRight or ArrowLeft, set the old item to -1,
// set the new item to 0, and call focus() on it.
</script>
Press Tab from here, and watch the count. Once inside, use the arrow keys, Home, and End.
Focus moves counted since you left the first button: 0. Last keys: (none).
In the roving pattern, only one button is in the Tab order at a time (the active one has tabindex 0, and the others have -1). Tab goes in once and out once, and the arrow keys move inside. This is the pattern recommended for toolbars, tabs, and menus.
Other Keyboard Rulesβ
- No keyboard-only features hidden behind hover or drag. Anything you can do with a mouse hover, drag, or gesture needs a keyboard and a simple pointer alternative (WCAG 2.1.1, 2.5.1, 2.5.7).
- Single-character shortcuts (such as pressing just "S" to search) can fire by accident, especially for people who use speech input. WCAG 2.1.4 asks that you let users turn them off, remap them, or only activate them when the relevant control has focus. Prefer shortcuts with a modifier key, and do not clash with the browser or screen reader shortcuts.
- Avoid
accesskey. It conflicts with browser and assistive technology shortcuts, and differs across platforms. - Do not change focus unexpectedly when a control receives focus or input (WCAG 3.2.1, 3.2.2). Moving focus must be a result of something the user did.
- Keep timed content manageable. If something disappears after a few seconds, a keyboard user may not reach it in time.
- Use
inerton background content when a modal is open. Theinertattribute makes an area unreachable by keyboard, mouse, and assistive technology. Support is good in modern browsers, but check the ones you need. - Hidden things must not stay focusable. Off-screen menus and collapsed panels should use
hidden,display: none, orinert, so Tab does not walk into invisible controls.
Test With Your Own Keyboardβ
You need nothing but your keyboard. Do this for each important page, and for every key task (sign-up, search, checkout):
- Put the mouse away.
- Start at the top, and press Tab repeatedly. Can you reach every link, button, and field?
- Is the focus always visible? Can you tell where you are?
- Is the order logical? Does it follow the visual layout?
- Can you activate everything with Enter or Space?
- Can you operate custom widgets with arrow keys, and close them with Esc?
- Can you leave every area? Are there any traps?
- When something appears or disappears (a menu, a dialog, an error), does focus go somewhere sensible?
- Use Shift + Tab to go backwards, and check it again.
- Repeat with the page zoomed to 200 percent.
Good Habitsβ
- Use native elements:
a,button,input,select,details,dialog. - Keep the source order the same as the visual order.
- Use
tabindexonly as 0 or -1. - Never remove the focus outline without a clearer replacement.
- Give every pointer action a keyboard alternative.
- Use roving tabindex for toolbars, tab lists, and menus.
- Move focus on purpose when views change, and return it when you close something.
- Avoid single-key shortcuts, or let users turn them off.
- Test with the keyboard on every release.
Accessibility Checklistβ
- Every interactive element is reachable with Tab, and can be used with Enter or Space.
- The focus order follows the visual and reading order.
- No
tabindexis greater than 0. - Focus is clearly visible, with enough contrast, and not hidden behind sticky bars.
- There are no keyboard traps.
- Custom widgets follow the expected keyboard patterns, and close with Esc.
- Anything that needs hover, drag, or a path gesture has a simple alternative.
- Hidden or off-screen content is not focusable.
- Focus moves to the right place after dialogs, menus, and page changes.
- Skip links work, and appear when focused. See Semantic Accessibility.
Security and Privacy Notesβ
- Do not trap focus to hold people on a page. Keyboard traps used to stop people leaving, to force them to read an advert, or to block navigation away, are both an accessibility failure and a dark pattern.
- Be careful with key listeners. A script that listens for every key press on the page (a "keylogger") can capture passwords and personal data. Listen only on the elements you need, and never send raw key presses to a server.
- Do not detect keyboard or switch use to profile people. How someone navigates can reveal a disability.
- Do not block paste or autofill in fields. It hurts people who use assistive input and password managers, and it makes passwords weaker. See WCAG 3.3.8.
- Clickjacking and focus: make sure that critical buttons, such as "Delete" or "Pay", cannot be focused and activated while hidden or covered by something else.
- Sensitive shortcuts (such as delete or send) should need a confirmation, so an accidental key press does no harm.
Global Audience Notesβ
- Keyboard layouts differ around the world. Do not rely on a key that exists only on some layouts, or on character keys that are hard to reach, for essential shortcuts.
- Right-to-left languages: the arrow key directions should match the reading direction. In a right-to-left toolbar, the Right Arrow often moves to the previous item. Test it, and use logical thinking, not fixed directions.
- Input methods: people typing in Chinese, Japanese, or Korean use an input method editor, where Enter confirms text rather than sending a form. Do not submit forms on a single Enter in a text field without checking that the text is not still being composed.
- Many users share computers or use older devices, where only the keyboard works properly. Lean, native HTML works best.
- Voice control users say what they see ("click Subscribe"). A visible label that matches the accessible name makes this work in any language (WCAG 2.5.3).
- Avoid shortcuts that clash with the screen readers and operating systems used in your audience's region.
Common Mistakes to Avoidβ
divandspanwith a click handler, and no keyboard support.- Removing the focus outline.
- Positive
tabindexvalues. - A visual order that disagrees with the code order.
- Menus that open only on hover.
- Modals with no way to close them using Esc.
- Focus lost after something is removed or replaced.
- Off-screen content that can still be tabbed to.
- Single-key shortcuts that cannot be turned off.
- Disabling a control with no explanation, and removing it from the Tab order.
- Custom widgets that ignore the usual arrow key patterns.
- Never testing with the keyboard.
Practice Timeβ
Try these in your browser and your editor:
- Use the focus order lab with each option, and explain what you saw.
- Take a site you use, put the mouse away, and try to finish one task.
- Remove
outline: nonefrom your CSS, and add a clear:focus-visiblering. - Replace a
divwith anonclickby a realbutton, and compare the keyboard behaviour. - Build a small toolbar with roving tabindex, using script.
- Open a native
<dialog>withshowModal(), and test Tab and Esc. - Add
scroll-padding-topto a page with a sticky header, and test with Tab. - Find a menu that opens on hover, and rewrite it so a keyboard user can open it.
- List every single-key shortcut in an app you use. Can you turn them off?
- Bonus: press Tab through your whole site, and write down every place where the focus is lost or unclear.
Quick Recapβ
Click a question to check your answer.
1. Which elements can be reached with Tab by default?
Links with an href, buttons, form controls (unless disabled), summary, and elements with a tabindex. A plain div or span cannot.
2. Which tabindex values should you use?
Only 0 (add to the natural order, when no native element fits) and -1 (focus by script, not by Tab). Never use a value above 0.
3. What is the difference between :focus and :focus-visible?
:focus applies for every kind of focus, including mouse clicks. :focus-visible applies when the browser decides focus should be visible, mostly for keyboard use and in text fields.
4. What is a keyboard trap?
A place where keyboard focus can enter, but cannot leave using the keyboard. It is a serious failure of WCAG 2.1.2.
5. What is roving tabindex?
A pattern for groups such as toolbars and tabs. Only the active item has tabindex="0", the others have -1, and arrow keys move the 0 and the focus. The whole group is one Tab stop.
6. Why can a CSS row-reverse cause an accessibility problem?
It changes the visual order without changing the code order. Focus follows the code, so it appears to jump around the screen, which confuses keyboard users.
Key Takeawaysβ
- Everything must work with a keyboard. It is the baseline for many kinds of assistive technology.
- Native elements (
a,button,input,select,details,dialog) give you the keyboard behaviour for free. - Use
tabindexonly as 0 or -1, and keep the source order the same as the visual order. - Never remove the focus outline. Style
:focus-visiblewith a clear, high-contrast ring. - Avoid traps. Everything focusable must be escapable, and dialogs must close with Esc.
- Custom widgets need the expected arrow key patterns, and roving tabindex for groups.
- Move focus on purpose, and return it when something closes.
- Test with just your keyboard, at 200 percent zoom, on every release.
Next, learn how to move and manage focus on purpose in Focus Management.