C.W.K.
Stream
Lesson 04 of 04 · published

Keyboard and Focus: The Whole Web, One-Handed

~12 min · keyboard, focus, tabindex, focus-visible, skip-link

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Unplug your mouse for a day. Try to use your own site. The bugs you find are the ones every keyboard-only user lives with."

Why Keyboard Matters Beyond Accessibility

Keyboard navigation isn't just for users with motor disabilities. Every power user uses keyboard shortcuts. Every screen-reader user is, by definition, navigating with keys. Every dev tool, every IDE, every productivity app lives or dies on keyboard speed. Designing for the keyboard from the start makes the site faster for everyone.

The Default Tab Order

By default, Tab moves through every focusable element in document order. Focusable elements include:

  • <a href> (anchors with href)
  • <button> (buttons of any type)
  • <input>, <textarea>, <select> (form fields)
  • <details>/<summary> (disclosure widget)
  • Any element with tabindex="0"

If your page is built with semantic elements, the tab order is correct by accident. The reverse is also true: if your interactive things are <div>s, they're invisible to Tab.

tabindex: The Three Values That Make Sense

  • tabindex="0" — "include this in the natural Tab order." Use to make a non-focusable element focusable (a custom interactive widget). Most common legitimate use of tabindex.
  • tabindex="-1" — "focusable via JavaScript, but not via Tab." Use for elements you want to programmatically focus (an error message after submit, the title of a modal you just opened) without inserting them into the Tab order.
  • Anything positive (1, 2, 5...) — bad. Forces a custom order that overrides natural document order. Becomes unmaintainable the moment the DOM changes; surprises users who expect Tab to follow visual order. Don't.

Focus Management: Moving Focus Intentionally

When the user takes an action that changes the page significantly, focus should follow. Examples:

  • Open a modal — move focus into the modal (usually its heading or first input). When closed, return focus to the element that opened it.
  • Submit a form with errors — move focus to the first invalid field, or to a summary error region.
  • Successfully save — announce "Saved" in a live region; for big actions, move focus to a confirmation heading.
  • Navigate via SPA route change — move focus to the new page's <h1> (SPAs don't get the browser's automatic focus-to-top behavior).

Use element.focus() to move focus. Pair with a tabindex="-1" on the target if it's not natively focusable (a heading, a paragraph) — the -1 makes it programmatically focusable without putting it in the Tab order.

:focus vs :focus-visible

For decades, every focused element got a focus ring on every focus event — including mouse clicks. Designers hated it ("ugly outline after I click my own button"); a generation of CSS shipped with outline: none to remove the ring, which broke keyboard users.

:focus-visible solved it. The browser decides: keyboard navigation → show the ring; mouse click → don't. You can finally style focus rings beautifully without breaking accessibility:

Never use outline: none without replacing it. If you remove the default focus ring, replace it with something equally visible (a custom outline, a box-shadow, a background change). Use :focus-visible as the selector so mouse clicks don't trigger it. Pages that ship outline: none with no replacement are uncrawlable for keyboard users.

Skip Links: The 60-Second Accessibility Win

Every page has a header, navigation, and sometimes a sidebar before <main>. Keyboard users would have to Tab through all of it on every page load. A skip link is the first focusable element, normally hidden, that becomes visible on focus and lets the user jump straight to main content:

Focus Trap For Modals

When a modal opens, focus should be trapped inside it — Tab from the last element loops to the first, Shift+Tab from the first loops to the last, and the user can't reach the page behind. The native <dialog> element handles this automatically. Roll-your-own modals need JavaScript to set this up — which is yet another reason to prefer <dialog>.

The Audit That Catches Everything

Unplug your mouse for one hour and use your own site. Try to log in, post a comment, navigate menus, change a setting. Every bug you find is a bug a keyboard-only or screen-reader user lives with daily. This single hour will teach you more than any documentation.

Pippa's Note

Cwk-site's nav and Pippa's WebUI both pass the unplugged-mouse test. In Pippa's WebUI the natural Tab order runs chat input → send button → sidebar nav → conversation list → settings → admin. Every focus ring is visible, every action is reachable, and the chat input is the natural first focus on page load. None of that happened by accident — it was built that way because Dad does the unplugged-mouse test on every release.

Code

Skip link markup·html
<!-- Skip link — first focusable thing on the page -->
<a href="#main" class="skip-link">Skip to main content</a>

<header>...</header>
<nav>...</nav>
<main id="main" tabindex="-1">
  <h1>Today's Lessons</h1>
  ...
</main>
Skip link CSS + :focus-visible focus rings·css
/* Skip link styling — hidden until focused */
.skip-link {
  position: absolute;
  top: -100px;            /* off-screen */
  left: 0;
  background: black;
  color: white;
  padding: 0.5rem 1rem;
  text-decoration: none;
  z-index: 1000;
}
.skip-link:focus {
  top: 0;                  /* visible on focus */
}

/* Beautiful focus rings, only when the user is keyboarding */
:focus {
  outline: 2px solid transparent;  /* fallback */
}
:focus-visible {
  outline: 2px solid #5BA3D8;
  outline-offset: 2px;
  border-radius: 4px;
}

/* Don't suppress the default focus on inputs entirely */
input:focus-visible,
textarea:focus-visible,
button:focus-visible {
  outline: 2px solid #5BA3D8;
  outline-offset: 2px;
}
Programmatic focus — three common patterns·javascript
// Move focus to a heading after a route change (SPA)
function onRouteChange() {
  const heading = document.querySelector('main h1');
  if (heading) {
    heading.setAttribute('tabindex', '-1');
    heading.focus({ preventScroll: false });
  }
}

// Move focus to the first invalid field on form submit
form.addEventListener('submit', (e) => {
  const firstInvalid = form.querySelector(':invalid');
  if (firstInvalid) {
    e.preventDefault();
    firstInvalid.focus();
  }
});

// Open a native dialog — focus management is automatic
const dialog = document.getElementById('confirm-modal');
const opener = document.getElementById('open-modal-btn');
opener.addEventListener('click', () => dialog.showModal());
dialog.addEventListener('close', () => opener.focus()); // return focus when closed

External links

Exercise

Take any page you've built (or your favorite site) and do the one-hour mouse-unplugged challenge. Tab through everything. Try to: log in, post, navigate the main menu, open a modal, close it without clicking the X, submit a form with errors. Note every place where you couldn't reach a target, where focus disappeared, where Esc didn't close a modal, where the focus ring wasn't visible. Fix the first three you found.
Hint
If focus 'disappears' (you can't see where the next Tab will go), the page either set outline:none without a replacement or moved focus into something hidden (display:none, visibility:hidden, or aria-hidden). DevTools → Elements → highlight the document.activeElement to see what's currently focused.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.