Skip to content

A button cannot be reached with Tab

A pointer-only control prevents keyboard users from saving their work.

WCAG 2.1.1WCAG 4.1.2 Test with KeyboardScreen readerZoomForced colors Markdown for agents
Broken Tab skips it.
Fixed Tab reaches it.
The same control broken and fixed.
01

The problem

Keyboard users cannot save their work.

What happens Tab skips the Save control. Read moreRead less

A settings screen has a Save control styled like a button. Clicking it saves the form, but pressing Tab moves from the last field straight to the footer.

Why it fails A div with a click handler, not a button. Read moreRead less

The control is a div with a click handler. It has no native button role or keyboard activation.

Who is affected Keyboard and voice users, stuck before Save. Read moreRead less

A keyboard user cannot complete the same action available to a pointer user. Voice commands may also lack a reliable control name.

What automation and AI miss role="button" passes a scan. Enter still does nothing. Read moreRead less

A scanner may flag a clickable element, but a clean scan after adding role="button" does not show that Enter and Space work once, focus is visible, or the save result is communicated. Generated markup still needs interaction testing.

02

Try it

Add it to the cart with Tab and Enter.

Tab to “Add to cart”, then press Enter or Space.

In cart: 0

A recreated demo. The broken version is intentionally inaccessible.

03

The fix

Replace the div with a native button, and keep its look.

  1. 1Use type="button" for a local action and type="submit" to send a form.
  2. 2Keep the existing styles on the button; reset only what conflicts.
  3. 3Announce the result in a status region that is already on the page.
Before A div that only listens for clicks
<div class="save-control" onclick="saveSettings()">Save changes</div>
The patch that is not a fix
<div class="save-control" role="button" tabindex="0" onclick="saveSettings()">Save changes</div>
After A native button

html

<button type="button" class="save-control">Save changes</button><p id="save-status" role="status"></p>

css

.save-control {  font: inherit;  /* Keep the existing padding, colors and radius here. */}.save-control:focus-visible {  outline: 3px solid var(--focus-ring);  outline-offset: 3px;}

js

const button = document.querySelector('.save-control');button.addEventListener('click', async () => {  await saveSettings();  document.getElementById('save-status').textContent = 'Settings saved.';});
Why this works

A div is not in the tab order, has no role, and ignores Enter and Space. It looks like a button only to people who can see it and use a pointer.

A click handler on a button also runs for Enter and Space, so no key handling is needed. The status region must be in the page before its text changes, or the result may not be announced.

The patch that is not a fix

This is the change scanners and code assistants often suggest. The control can now be focused and is announced as a button, but Enter and Space still do nothing. To finish the job by hand you would need key handlers, Space firing on key release, scroll prevention and a disabled state. A native button already does all of this.

Implementation decisions

A button inside a form submits by default. Use type="button" for a local action and type="submit" for submission. For a toggle, keep the label stable and expose aria-pressed. If the control takes the user to another page, use a link instead.

Keep the existing font, padding and shape on the native element. Browsers add their own button styles, so reset what conflicts with your design rather than going back to a div.

To stop the problem returning, make the native button the default in your component library. In code review, ask whether a control performs an action or navigates to another page: a button for an action, a link for navigation.

04

Verify the fix

5 checks, no mouse.

0 of 5 checked

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared button component changes.

Limits

This is a starting pattern, not a guarantee for every application. The snippets omit application-specific data handling. Test the complete journey with your target assistive technology.

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

References: www.w3.org

Keep this fixed as your code changes.

Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.

How retests work