Command Palette

Search for a command to run...

Hectal
PHASE 7Advanced ~27 min· topic 3 of 8

Topic 7.3

Keyboard and Focus

In one line

Every control must work with the keyboard alone: interactive elements are focusable in source order, tabindex 0 and -1 adjust that, a skip link jumps past repeated menus, and focus must always be visible.

Think of it like this

A TV remote with only arrow and OK buttons. A highlight box moves from tile to tile, and you always need to see where it is. If the box disappears, or some tiles can't be reached, the remote is useless. Keyboard users browse the web the same way: the highlight is the focus.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Focus
The marker for the one element that receives key presses right now.
Focusable
Able to receive focus, with Tab or with JavaScript's focus().
Tab order
The order focus moves through elements when you press Tab. By default, the order they appear in the HTML.
tabindex
An attribute that makes an element focusable (0), focusable only by script (-1), or (badly) reorders focus (positive numbers).
Skip link
A link at the very top that jumps to the main content, so keyboard users can skip the menu.
Focus ring
The outline the browser draws around the focused element.
:focus-visible
A CSS selector that matches focused elements when the browser decides a ring should show, mainly for keyboard focus.
Keyboard trap
A place where focus gets stuck and Tab can't move out. WCAG 2.1.2 forbids it.
document.activeElement
The JavaScript property that holds the element that has focus right now.

Step by step

01Press Tab on a plain page

Focus starts on the page itself. Each Tab press moves to the next focusable element in source order. Links, buttons and fields are in the list; paragraphs and headings are not (screen reader users move through those with other keys, like H for headings).

Press Tab on a plain pagediagram
Rendering diagram…

02Add a skip link

Put the link first in <body> and give <main> the matching id. Following an in-page link moves focus to the target (or to its position, so the next Tab continues from there).

index.htmlwhole filehtml
<body>
  <a class="skip" href="#main">Skip to main content</a>
  <header>
    <nav><!-- 20 menu links --></nav>
  </header>
  <main id="main">
    <h1>Course catalogue</h1>
  </main>
</body>

03Hide it until it gets focus

Moving it off-screen keeps it in the Tab order; :focus brings it back into view. Don't use display: none or visibility: hidden: those make it unfocusable.

style.csswhole filecss
.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 8px;
  top: 8px;
  padding: 8px 12px;
  background: #fff;
  z-index: 10;
}

04Keep the focus ring, make it better

If the design team dislikes the default ring on mouse clicks, style :focus-visible instead of deleting outlines. A 2–3 px solid outline with a small offset, in a colour with 3:1 contrast against the background, works on almost any design.

style.csswhole filecss
:focus-visible {
  outline: 3px solid #1a56db;
  outline-offset: 2px;
}
/* never this on its own: */
/* *:focus { outline: none; } */

05Move focus on purpose with tabindex="-1"

After an action that changes a large part of the page (loading search results, moving to the next step of a form), move focus to the new heading so keyboard and screen reader users start there. tabindex="-1" lets the heading take focus from script without adding it to the Tab order.

results.htmlwhole filehtml
<h2 id="results-title" tabindex="-1">12 results for "flexbox"</h2>
<script>
  document.getElementById("results-title").focus();
</script>

06Test it

Unplug the mouse (or don't touch it) and Tab through the page from top to bottom. Check: can you reach every control, is the order logical, can you always see the ring, can you operate everything with Enter, Space, arrows and Esc, and can you always Tab out again?

Try it yourself

  1. 1

    Predict what can take focus

    In the first example, add <details><summary id="sum">More</summary>Hidden text</details>, <textarea id="ta"></textarea> and <span id="edit" contenteditable>Edit me</span>. Predict which are focusable, then run it. (All three are.)

  2. 2

    Break the visual order

    In the second example's CSS, add nav { display: flex; flex-direction: row-reverse; justify-content: flex-end; }. Click in the preview and Tab through the menu: the links appear right-to-left visually but focus still goes in source order. This mismatch is a WCAG 2.4.3 problem.

  3. 3

    Remove the ring and feel the pain

    Replace the :focus-visible rule with * { outline: none; }, click in the preview and Tab around. Try to tell where you are. Then put it back.

Code & diagrams

Which elements are focusable? New tab

Surprise: the `tabIndex` property says 0 for every `a` and `button`, even a link without href and a disabled button, because the spec gives those elements a default of 0. Calling `focus()` and checking `document.activeElement` is the real test. The heading is focusable by script but skipped by Tab. Click in the preview and press Tab to walk the real order.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

link | focusable: true | tabIndex: 0
nohref | focusable: false | tabIndex: 0
btn | focusable: true | tabIndex: 0
off | focusable: false | tabIndex: 0
field | focusable: true | tabIndex: 0
plain | focusable: false | tabIndex: -1
zero | focusable: true | tabIndex: 0
heading | focusable: true | tabIndex: -1
A skip link and visible focus New tab

Removing the outline on `main` is fine because it's a landing spot, not something you operate. Never remove it from links, buttons or fields.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

focus -> Skip to main content
focus -> main
Keys every user expectsdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Delete every focus outline

Add *:focus { outline: none; } to the stylesheet (a common "reset" line).

terminal
$ Tab through the page
── what you'll see ──
Focus moves (the page scrolls, Enter still works) but nothing shows where it is

Break #2

Use a positive tabindex

Give the newsletter field tabindex="1" so it gets focus first.

terminal
$ Tab from the top
── what you'll see ──
Focus jumps to the newsletter field in the footer first, then back to the skip link and header

Break #3

Hide the skip link with display: none

Write .skip { display: none; } and .skip:focus { display: block; }.

terminal
$ Tab from the top
── what you'll see ──
The skip link never appears; focus goes straight to the first menu link

Myth vs fact

Myth

Screen reader users press Tab to read the page.

Fact

Tab only visits interactive elements. Screen reader users read with their screen reader's own keys: arrows line by line, H for headings, D (NVDA) for landmarks, K for links.

Myth

Adding tabindex="0" makes a div accessible.

Fact

It only makes the div focusable. It still has no role, no name and no Enter or Space behaviour. Use a real <button> or <a>.

Myth

Focus outlines are ugly, so designers are right to remove them.

Fact

Removing them breaks the page for keyboard users. :focus-visible lets you show a well-designed ring only for keyboard focus.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    :focus-visible uses browser heuristics: it matches after keyboard navigation, always for text inputs (because typing follows), and usually not after a mouse click on a button. When script calls focus(), browsers generally show the ring if the last interaction was from the keyboard. Firefox, Chrome and Safari have all supported it since 2022.

  • ▸

    The **inert** attribute (supported in all major browsers since 2023) removes a whole subtree from focus, clicks and the accessibility tree. dialog.showModal() makes everything outside the dialog inert automatically and returns focus to the opener when it closes; build modals on <dialog> rather than writing your own focus trap.

  • ▸

    Roving tabindex is the pattern for composite widgets (tabs, toolbars, menus, grids): only one item has tabindex="0", the others -1; arrow keys move focus and swap the values. The whole widget is one Tab stop, which matches how native radio groups behave. The ARIA Authoring Practices Guide (APG) documents the keys for each widget.

  • ▸

    WCAG 2.2 added 2.4.11 Focus Not Obscured (Minimum) (AA): the focused element must not be entirely hidden by author content such as a sticky header or cookie banner. CSS scroll-padding-top on the scroll container keeps focused items from sliding under a fixed header.

Remember this

  1. 1

    Focus is the browser's marker for which element receives keyboard input right now. Only one element has focus at a time; JavaScript can read it as document.activeElement. Many people navigate by keyboard: blind screen reader users, people with motor disabilities, switch and voice-control users (their tools send key presses), and power users. WCAG 2.1.1 Keyboard (A) says all functionality must work from a keyboard.

  2. 2

    The keys: Tab moves focus forward, Shift+Tab backward. Enter follows a link and presses a button. Space presses a button and toggles a checkbox. Arrow keys move inside a group: between radio buttons with the same name, through <select> options, in sliders. Esc closes dialogs and popovers. Native elements do all of this for free; a custom widget has to rebuild it with JavaScript.

  3. 3

    Focusable elements by default: links with an href, <button>, <input> (except type="hidden"), <select>, <textarea>, <summary>, <iframe>, <audio> and <video> with controls, and anything with contenteditable. Disabled form controls are not focusable. A link without href and plain div/span are not focusable.

  4. 4

    Tab order is the order elements appear in the HTML source (the DOM order), not where CSS places them on screen. If CSS (order, flex-direction: row-reverse, grid placement, position: absolute) moves things around, focus can jump in a confusing zigzag: WCAG 2.4.3 Focus Order (A). Fix the source order rather than fighting it.

  5. 5

    The tabindex attribute changes focus behaviour. tabindex="0" makes an element focusable and puts it in the Tab order at its source position (use it only on custom widgets that also have a role and keyboard handling). tabindex="-1" makes an element focusable by JavaScript (el.focus()) or by a link to its id, but not with Tab: perfect for headings or containers you move focus to. Positive values (tabindex="1" and up) jump ahead of everything else and create an order that's almost impossible to maintain: never use them.

  6. 6

    A skip link is the first link on the page, <a href="#main">Skip to main content</a>, pointing at the <main id="main"> element. Without it, keyboard users press Tab through the whole menu on every page (WCAG 2.4.1 Bypass Blocks, A). It can be hidden until focused with CSS, but must appear on screen when it gets focus. And focus must be visible: the browser draws a focus ring; never remove it with outline: none without a replacement (WCAG 2.4.7 Focus Visible, AA). The CSS :focus-visible pseudo-class shows a ring for keyboard focus without showing it on every mouse click.

Explain it without notes

01

Which elements are focusable by default, and how does tabindex change that?

02

What is a skip link, why is it needed, and how do you hide it without breaking it?

03

Why is outline: none dangerous, and what should you do instead?

04

What is a keyboard trap? Give an example and how to avoid it.

Practice

01

Write a page header with a skip link, a nav of three links and a main with an h1, plus the CSS that hides the skip link until it's focused.

02

Write CSS that shows a 3 px blue outline with a 2 px offset for keyboard focus on every element, without showing it after mouse clicks on buttons.

03

A product card is <div class="card" onclick="openProduct()">. Rewrite it so it works with the keyboard without adding JavaScript key handlers.

Trade-offs

  • ↔

    Moving focus after an action helps keyboard and screen reader users find new content, but moving it too often is disorienting; move it only when the user's context really changes (new page section, dialog opened or closed, error summary).

  • ↔

    Hiding the skip link until focus keeps the design clean but sighted mouse users never learn it exists; some sites show it always, which is also fine.

Done when you can

  • I can list the natively focusable elements and test focus with focus() and document.activeElement.

  • I use tabindex="0" and "-1" correctly and never positive values.

  • I can add a skip link that appears on focus.

  • I keep focus visible and style it with :focus-visible.

  • I can test a page with Tab, Shift+Tab, Enter, Space, arrows and Esc.

  • I keep the DOM order the same as the visual order.