Keyboard Accessibility: Best Practices for Web Developers
Not every user navigates the web with a mouse. Power users, people with motor disabilities, and anyone working through a form quickly rely on the keyboard to get things done. Good keyboard accessibility is not just an accessibility requirement - it is a sign of a well-crafted interface. Sometimes I'm too lazy to reach for the mouse, and my hands are already on the keyboard. Here is what you need to know to get it right.
1. Use Semantic HTML First
The single biggest improvement you can make is using the right HTML element for the job. Browsers give <button>, <a>, <input>, <select>, and other interactive elements keyboard support for free. They are focusable by default, they respond to Enter and Space, and screen readers understand their purpose.
A <div> with a click handler gives you none of that (thankfully we're seeing less of this nowadays). You then have to manually add tabindex="0", a keydown listener, and ARIA attributes to recreate what a <button> already does. Save yourself the work and reach for semantic elements first.
<!-- Avoid -->
<div class="btn" onclick="submit()">Submit</div>
<!-- Prefer -->
<button type="submit">Submit</button>
2. Never Remove Focus Indicators
Browsers display an outline around the focused element by default. It is common to see outline: none or outline: 0 in CSS resets - usually added because the default ring clashes with a custom design. The result: keyboard-only users have no visual cue about where they are on the page.
The fix is not to keep the browser default but to replace it with something that fits your design. Removing focus indicators is one of the most damaging things you can do to keyboard accessibility:
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
Using :focus-visible instead of :focus means the custom ring only appears during keyboard navigation, not on mouse clicks. That gives you the best of both worlds.
3. Add a Skip Navigation Link
When a keyboard user lands on a page, they must tab through every header link before reaching the main content. On a site with a large navigation menu, that is exhausting. A skip link is a small but important keyboard accessibility improvement. It lets users jump straight to the content with one keypress.
Place it as the very first element in the <body> and keep it visually hidden until focused:
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content">...</main>
.skip-link {
position: absolute;
top: -100%;
}
.skip-link:focus {
top: 0;
}
4. Manage Tab Order Intentionally
The default tab order follows the DOM order, which is usually correct if your HTML is structured logically. Problems arise when CSS positioning or JavaScript-injected content puts elements visually in one place and structurally in another.
Avoid using tabindex values greater than 0 to force a custom order. You're creating a parallel tab sequence that is hard to maintain and frequently breaks. Instead, fix the DOM order to match the visual order. Reserve tabindex="0" for making non-interactive elements focusable, and tabindex="-1" for elements you want to focus programmatically (such as a modal heading) without putting them in the natural tab sequence.
5. Follow Established ARIA Keyboard Patterns
Custom components - dropdown menus, date pickers, tabs, carousels - need keyboard behavior that matches what users expect. This is a core part of keyboard accessibility for complex UIs. The ARIA Authoring Practices Guide (APG) defines these patterns precisely. Some common ones:
- Tabs: Arrow keys move between tab panels; Tab moves focus into the panel content.
- Modal dialogs: Focus is trapped inside the modal while it is open; Escape closes it and returns focus to the trigger.
- Combo boxes: Arrow keys navigate the list; Enter selects; Escape collapses.
Implementing these patterns consistently means users do not have to guess how your components work. If you are using a component library, check whether it follows the APG before shipping - many do not.
6. Return Focus After Dynamic Changes
When something in the UI changes dynamically - a modal opens, a panel expands, content is deleted - focus needs to go somewhere sensible. If you open a modal and focus stays behind it on the page, keyboard users are stuck. If you delete a list item and focus disappears entirely, they have to start navigating from the top. It can become cumbersome for users who rely on keyboards.
A few rules of thumb:
- When opening a modal or drawer, move focus to the first focusable element inside it (or the heading if there is one).
- When closing a modal, return focus to the element that triggered it.
- When deleting an item from a list, move focus to the next item, or the previous one if it was the last.
// After opening a modal
modalRef.current.querySelector('h2').focus();
// After closing
triggerButtonRef.current.focus();
Testing Keyboard Navigation
Before shipping any interactive feature, unplug your mouse and tab through it. Check that every interactive element is reachable, that focus is always visible, and that the flow makes sense without a visual context clue. Can you see where the tab is or has gone? Does moving from one object to another make sense? Browser DevTools can highlight focusable elements, and extensions like axe or Wave catch common issues automatically. The most reliable test, though, is still a real keyboard in your hands as you tab through your site.
Keyboard accessibility is not a checkbox to tick at the end of a project. Build it in from the start by choosing semantic elements, maintaining visible focus, and testing early. You will rarely have to retrofit it later. Logically structured HTML often results in cleaner, more maintainable markup too - and good accessibility and good code quality tend to go hand in hand.
Comments (0)
No comments yet — be the first to share your thoughts.
Leave a comment