Section 3 · Keyboard, Focus and ARIA
Keyboard Access and Visible Focus
18 min4 min readCode playground
Lesson at a glance
Make every Ticketly control reachable in a logical order with the keyboard, show a clear focus indicator, add a skip link, and keep focus from hiding behind sticky bars.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Put your mouse in a drawer and try to buy a Friday pass on Ticketly. You will discover three things in about thirty seconds. Tab takes you through forty-odd header and footer links before reaching the festival. Focus then jumps from the Saturday button to the Friday button and back to the top. And for most of the trip you can’t see where focus is at all, because someone wrote outline: none in 2019 to get rid of “that ugly blue box”.
Keyboard access isn’t a niche. Everyone using a screen reader on a desktop relies on the keyboard, and so do switch users, people with tremor, people whose trackpad broke, and developers who simply prefer it. WCAG 2.1.1 Keyboard (A) requires that everything works from the keyboard, and 2.1.2 (A) that focus never gets trapped.
The keys people use
- Tab / Shift+Tab: move between interactive elements (links, buttons, form fields).
- Enter: follow a link, activate a button, submit a form.
- Space: activate a button, toggle a checkbox, scroll the page when nothing is focused.
- Arrow keys: move within a widget, such as radio buttons, a select, tabs or a menu.
- Escape: close a dialog, menu or popup.
Native elements already support all of this. Your job is mostly not to break it, and to match these conventions when you build custom widgets in Widget Patterns.
Focus order follows the DOM
Tab moves through focusable elements in source order. If the visual layout puts the “Add Saturday” button before “Add Friday” but the markup has them the other way round, focus appears to jump backwards. The fix is to put the markup in the order you want people to meet it. Be wary of CSS that rearranges things visually, such as order, flex-direction: row-reverse or explicit grid placement, because focus will still follow the markup (2.4.3 Focus Order).
tabindex adjusts focusability:
| Value | Effect | Use it for |
|---|---|---|
0 |
Focusable, in DOM order | A custom control built from a non-interactive element |
-1 |
Focusable from script only, not with Tab | Headings or containers you move focus to |
1 or more |
Jumps ahead of everything else | Never |
Make focus visible
A keyboard user without a visible focus indicator is like a mouse user with an invisible pointer. WCAG 2.4.7 Focus Visible (AA) requires one; 1.4.11 Non-text Contrast means it should have at least 3:1 contrast against what surrounds it.
The modern tool is :focus-visible. Browsers apply it when focus should be shown: always for keyboard navigation and text fields, but usually not when a button is clicked with a mouse. That answers the designer’s real complaint without hurting anyone:
:focus-visible {
outline: 3px solid #5b3cc4;
outline-offset: 2px;
}Use outline rather than box-shadow for the ring. Windows contrast themes (forced colors mode) remove box shadows but keep outlines, so a shadow-only indicator vanishes for the people who most need it.
Skip links and sticky bars
Ticketly’s header has a logo, ten menu links, a search field and an account menu: thirteen Tab stops. A keyboard user has to tab through all of them on every page. WCAG 2.4.1 Bypass Blocks (A) asks for a way past repeated content, and the classic solution is a skip link: the first focusable element on the page, hidden until it receives focus.
<body>
<a class="skip-link" href="#main">Skip to main content</a>
<header>…</header>
<main id="main">…</main>
</body>.skip-link {
position: absolute;
inset-block-start: 8px;
inset-inline-start: 8px;
padding: 8px 12px;
background: #fff;
transform: translateY(-150%);
}
.skip-link:focus {
transform: none;
}Don’t hide it with display: none, which would remove it from the tab order. Moving it off-screen and back on focus keeps it reachable and makes it appear exactly when a keyboard user needs it.
Sticky headers and cookie banners bring the opposite problem: focus moves to an element that is hidden underneath them, which fails 2.4.11 Focus Not Obscured (AA). Tell the browser how much of the viewport the sticky parts cover, and it will scroll focused elements clear of them:
html {
scroll-padding-block-start: 96px; /* sticky header height plus a little air */
}For a cookie banner, the real fix is not to let it float over content indefinitely: place it in the flow, or make sure it can be dismissed before anything else is reached.
Ticketly can now be crossed from top to bottom with a keyboard. The next lesson handles the harder cases, where your code has to move focus itself: dialogs and single-page app navigation.
Key takeaways
- Focus order follows the DOM order, so put markup in the order people should meet it, and don’t fight it with CSS reordering.
- Use tabindex=“0” to add a custom control to the tab order and tabindex=“-1” for elements you focus from script; never use a positive tabindex.
- Never remove the focus outline without a replacement; style :focus-visible with an indicator of at least 3:1 contrast.
- A skip link as the first focusable element lets keyboard users bypass repeated navigation, and scroll-padding stops sticky bars hiding focus.