EduFlow

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.

  1. 1The keys people use
  2. 2Focus order follows the DOM
  3. 3Make focus visible
  4. 4Skip links and 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:

ticketly/styles.css
: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.

HTML
<body>
  <a class="skip-link" href="#main">Skip to main content</a>
  <header>…</header>
  <main id="main">…</main>
</body>
CSS
.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.

Without a skip link, the first Tab press lands on the logo and twelve more presses cross the header before reaching main content. With a skip link, the first Tab shows Skip to main content, and Enter moves straight to main. Without a skip link … 13 stops main With a skip link Skip to main content Enter main
The first Tab press on Ticketly

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:

CSS
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.

Further reading

Check your understanding

4 questions

Question 1 of 4

In what order does Tab visit these buttons?

HTML
<button>Friday</button>
<button tabindex="2">Saturday</button>
<button tabindex="1">Sunday</button>
<button>Weekend</button>
Question 2 of 4

A designer finds the default focus ring ugly. Which CSS is the best response?

Question 3 of 4

When should an element get tabindex="-1"?

Question 4 of 4

Ticketly’s sticky header is 80px tall. When a keyboard user tabs backwards up the page, focused links slide underneath it. Which fix addresses 2.4.11 Focus Not Obscured?

Practice

Code playground

Make Ticketly keyboard friendly

Three keyboard problems on one small page:

  1. The pass buttons use positive tabindex values, so focus order jumps around. Remove them; the DOM order is already right.
  2. A 2019 rule hides every focus indicator. Replace it with a visible :focus-visible outline at least 2px wide.
  3. There is no way to bypass the header. Add a skip link as the first focusable element, with text containing “Skip” and href="#main". It may be hidden off-screen, but it must be visible while focused (don’t use display: none).
<header class="site-header">
  <a href="#" class="logo">Ticketly</a>
  <nav aria-label="Site">
    <a href="#events">Events</a>
    <a href="#faq">FAQ</a>
    <a href="#account">Account</a>
  </nav>
</header>
<main id="main">
  <h1>Harbour Lights 2026</h1>
  <div class="passes">
    <button tabindex="3">Add Friday pass</button>
    <button tabindex="1">Add Saturday pass</button>
    <button>Add weekend pass</button>
  </div>
</main>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • No element has a positive tabindex
  • The first focusable element is a skip link pointing to #main
  • The skip link is visible on screen while it has focus
  • Links and buttons show an outline at least 2px wide when focused