Keyboard, Focus, and ARIA

Introduction

Many users navigate pages without a mouse. They use the keyboard, switch devices, voice control, or assistive technology. This chapter explains focus order, focusable elements, skip links, tabindex, and basic ARIA attributes such as aria-label, aria-labelledby, aria-describedby, aria-expanded, and aria-hidden.

Prerequisites

What Focus Means

Focus is the current element ready to receive keyboard input.

Common focusable elements:

  • <a href="...">
  • <button>
  • <input>
  • <select>
  • <textarea>
  • <summary>
  • elements with tabindex="0" (use carefully)

Try this:

  1. Open a page.
  2. Press Tab.
  3. Watch which element gets focus.
  4. Press Enter or Space depending on the control.

If you cannot see where focus is, the page is hard to use.

Natural Tab Order

The browser follows DOM order:

Tab order:

  1. skip link
  2. Home
  3. Docs
  4. Open example

Keep DOM order logical. Do not use CSS layout tricks that make visual order wildly different from keyboard order.

Tip

Test Without a Mouse

Before publishing, put your mouse aside. Can you reach every interactive element, understand where you are, and activate controls?

Do Not Remove Focus Styles

Bad CSS pattern:

css
:focus {
  outline: none;
}

If you remove the default outline, replace it with a visible alternative:

css
:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 3px;
}

HTML provides focusable controls; CSS must make focus visible.

Skip links help keyboard users bypass repeated navigation:

CSS often hides skip links until focused:

css
.skip-link {
  position: absolute;
  left: -999px;
}
 
.skip-link:focus {
  left: 1rem;
  top: 1rem;
}

The link must become visible when focused.

tabindex

tabindex changes focus behavior.

ValueMeaning
0Add to natural tab order
-1Programmatically focusable, not reachable by Tab
Positive numberCustom order — avoid

tabindex="0"

Use rarely for custom interactive elements or regions that truly need focus:

html
<div tabindex="0">
  Keyboard-focusable region
</div>

But if the element is interactive, prefer a native element:

html
<button type="button">Open panel</button>

tabindex="-1"

Useful for moving focus after navigation:

html
<main id="main-content" tabindex="-1">
  <h1>Dashboard</h1>
</main>

JavaScript can focus it:

js
document.querySelector("#main-content").focus();

Avoid positive tabindex

html
<!-- Avoid -->
<input tabindex="3">
<input tabindex="1">

Positive values create a custom tab order that is hard to maintain and often confusing.

ARIA: What It Is

ARIA (Accessible Rich Internet Applications) adds accessibility information when native HTML cannot express a custom pattern.

Important rule:

text
Native HTML first. ARIA only when needed.

Good:

html
<button type="button">Save</button>

Avoid:

html
<div role="button" tabindex="0">Save</div>

The fake button still needs keyboard event handling and testing. The real button already works.

aria-label

Provides an accessible name when visible text is missing or insufficient:

html
<button type="button" aria-label="Close dialog">×</button>

Use it for icon-only buttons. If visible text exists and is good, do not duplicate it.

Bad:

html
<button aria-label="Submit form">Submit form</button>

The visible text already names the button.

aria-labelledby

Uses another element as the accessible name:

html
<section aria-labelledby="pricing-heading">
  <h2 id="pricing-heading">Pricing</h2>
  <p>Plans for every team.</p>
</section>

Useful when the label is already visible on the page.

aria-describedby

Adds extra description:

html
<label for="password">Password</label>
<input id="password" name="password" type="password" aria-describedby="password-help">
<p id="password-help">Use at least 8 characters.</p>

The label names the control. The description adds details.

aria-expanded

Indicates whether a collapsible region is open:

html
<button
  type="button"
  aria-expanded="false"
  aria-controls="menu"
>
  Menu
</button>
 
<nav id="menu" hidden>
  <a href="index.html">Home</a>
  <a href="about.html">About</a>
</nav>

JavaScript must update both the visual state and the ARIA state:

js
button.setAttribute("aria-expanded", "true");
menu.hidden = false;

If ARIA says open while the UI is closed, assistive technology receives wrong information.

aria-hidden

Hides content from the accessibility tree:

html
<span aria-hidden="true">★</span>
<span>Featured</span>

Use for decorative icons when nearby text already communicates meaning.

Do not hide focusable or meaningful content:

html
<!-- Bad: focusable link hidden from screen readers -->
<a href="checkout.html" aria-hidden="true">Checkout</a>

If something is interactive or important, it should not be aria-hidden.

Warning

ARIA Must Match Reality

ARIA attributes do not change visual behavior by themselves. If you use aria-expanded, aria-hidden, or roles, your JavaScript and CSS must keep the visual and accessibility states synchronized.

Accessible Names: Quick Examples

ElementAccessible name source
<button>Save</button>Button text
<img alt="Company logo">alt text
<input> with <label>Associated label
Icon buttonaria-label
Sectionaria-labelledby heading

When in doubt, inspect the element in DevTools Accessibility pane.

Keyboard Checklist

For each page:

  • Can you Tab to every interactive element?
  • Is focus visible?
  • Does tab order match visual/logical order?
  • Do links activate with Enter?
  • Do buttons activate with Enter/Space?
  • Can you escape or close custom dialogs/menus?
  • Does focus return to a sensible place after closing a modal?
  • Are hidden elements removed from the tab order?

Mini Exercise

Improve a simple navigation page:

  1. Add a skip link to <main id="main-content">.
  2. Tab through the page and note the order.
  3. Add a visible focus style with CSS.
  4. Add aria-current="page" to the active nav link.
  5. Create one icon-only button with aria-label.

FAQ

Should I add role="button" to <button>?

No. <button> already has the correct role.

Is tabindex="0" bad?

Not always, but native focusable elements are better for interactive controls. Use tabindex="0" only when you know why a non-native element must receive focus.

Can I hide text visually but keep it for screen readers?

Yes, with a tested visually-hidden CSS utility. Do not use display: none or aria-hidden="true" if screen readers need it.

Does ARIA change behavior?

No. ARIA changes accessibility information. It does not add keyboard handling, open menus, or manage focus.

What comes next?

HTML SEO basics — titles, meta descriptions, headings, semantic content, images, and links for search engines.