# Keyboard Users and Accessible Controls

Build controls that work for people who use a keyboard or a screen reader, with real buttons, aria-pressed and aria-expanded that match what the page shows, and an Escape key that closes what's open and puts focus back.

Journeyman · JavaScript · Web Development · Updated November 1, 2026

## After this lesson you can

- Choose a real button for a control, so it works with the mouse, the Tab key, Enter and the space bar
- Keep a toggle button's aria-pressed or aria-expanded in step with what the page shows
- Close an open panel with the Escape key and move focus back to the button that opened it

## Key takeaways

- Many people use a keyboard instead of a mouse. Tab moves to the next control, Shift+Tab goes back, and Enter or the space bar presses a button.
- A real button element works with the mouse, Tab, Enter and the space bar, and a click listener hears all of them. A clickable div works only with a mouse.
- aria-pressed says whether an on/off button is on. aria-expanded says whether the thing a button opens is open. Change the attribute every time the look changes.
- Escape should close what's open. Then element.focus() puts the keyboard back on the button that opened it, so nobody loses their place.

Not everyone uses a mouse. Someone with a broken wrist, someone with a tremor, someone who is blind and has a screen reader read the page aloud, someone who is faster on the keyboard than with a mouse: all of them move through a page with keys. A heart that only a mouse can press is a heart they can't press. Making every control work from the keyboard is part of building a page that works, and it's often a requirement too: on a class project's rubric, in a job's code review, and in the accessibility laws many countries have for websites.

The good news is that most of it comes from choosing the right element. This lesson shows what keyboard users expect, and the few lines of JavaScript that keep a page honest with them.

## Using a page without a mouse

Try this on any website: put the mouse aside and press **Tab**. A ring, the **focus outline**, jumps to the first link or button. Each Tab moves it to the next control, and **Shift+Tab** moves it back. The control with the ring has **keyboard focus**: it's the one your key presses go to. **Enter** follows a link or presses a button, and the **space bar** presses a button too. On a Mac, Safari and Firefox skip buttons and links when you press Tab, until you switch it on: in Safari, Settings > Advanced > Press Tab to highlight each item on a webpage (or press Option+Tab); in Firefox, turn on Keyboard navigation in macOS System Settings > Keyboard. Chrome and Edge reach them already.

The order Tab follows is the order of the controls in the HTML. In this filter bar, Tab goes All, Breakfast, Lunch, Dinner:

```html
<div class="filter-bar">
  <button>All</button>
  <button>Breakfast</button>
  <button>Lunch</button>
  <button>Dinner</button>
</div>
```

People who use a **screen reader**, a program that reads the page aloud, move the same way, and hear each control's name and what kind of control it is: "Breakfast, button". In your textbook this is called *web accessibility* (often written *a11y*: an a, 11 letters, a y). The guidelines most sites are measured against are WCAG, the Web Content Accessibility Guidelines, and "everything works from the keyboard" is one of them.

## A real button does the work for you

These two look the same once they're styled, and both answer a mouse click:

```html
<div class="favorite">♡ Favorite</div>
<button class="favorite">♡ Favorite</button>
```

Only one of them works for everyone:

| What you try | `<div>` | `<button>` |
|---|---|---|
| Mouse click | Works | Works |
| Tab reaches it | No: Tab skips it | Yes |
| Enter and space bar | Nothing happens | They press it, and your `click` listener runs |
| A screen reader says | "Favorite" | "Favorite, button" |

A `<button>` answers Enter and the space bar with a `click` event, so the `click` listener you already wrote works for keyboard users, with no extra code. Making a `<div>` behave the same takes a `tabindex` attribute, a `role` attribute and a `keydown` listener for two keys, and missing one of the three is a common mistake.

The rule: if it **does something** on this page, use a `<button>`. If it **goes to another page**, use a link, `<a href="...">`. One more thing to know: a `<button>` inside a form sends the form when pressed, unless you give it `type="button"`.

## aria-pressed: buttons that are on or off

A light switch shows you whether it's on. A heart that turns red does the same for someone who can see it, but a screen reader can't see red. It needs to be told.

**ARIA** attributes (from *Accessible Rich Internet Applications*) tell screen readers what a control means. **`aria-pressed`** marks a button that stays on or off, a **toggle button**: `"true"` when it's on, `"false"` when it's off. The screen reader then says "Favorite, toggle button, pressed".

```html
<button class="favorite" aria-pressed="false">♡ Favorite</button>
```

Your JavaScript changes the attribute every time it changes the look. `getAttribute('aria-pressed')` reads the attribute's value, which is always a string, and `setAttribute('aria-pressed', 'true')` sets it:

```javascript
const heart = document.querySelector('.favorite');

heart.addEventListener('click', () => {
  const isOn = heart.getAttribute('aria-pressed') === 'true';
  heart.setAttribute('aria-pressed', String(!isOn));
  heart.textContent = isOn ? '♡ Favorite' : '♥ Favorite';
});
```

The first click turns it on (`aria-pressed="true"`, a filled heart), and the second turns it off again. The word stays *Favorite* either way: the attribute says whether it's on, so the name doesn't need to. The filter buttons work the same: the one that's chosen has `aria-pressed="true"`, and the others `"false"`.

A stylesheet can style a button by its attribute, with a selector such as `.favorite[aria-pressed="true"]`. When the CSS does that, the look and the meaning can never disagree, because there's only one thing to change.

## aria-expanded: buttons that open something

The **Show steps** button doesn't turn itself on; it opens something else, the list of steps. That's what **`aria-expanded`** is for: `"true"` while the thing it controls is open. The steps start hidden, with the `hidden` attribute:

```html
<button class="show-steps" aria-expanded="false">Show steps</button>
<ol class="steps" hidden>
  <li>Slice the peppers and onions.</li>
  <li>Roast for 20 minutes.</li>
</ol>
```

`hidden` hides an element from everyone, screen readers included, and JavaScript sets it with `element.hidden = true` or `false`. Each click flips both, together:

```javascript
const showSteps = document.querySelector('.show-steps');
const steps = document.querySelector('.steps');

showSteps.addEventListener('click', () => {
  const isOpen = showSteps.getAttribute('aria-expanded') === 'true';
  showSteps.setAttribute('aria-expanded', String(!isOpen));
  steps.hidden = isOpen;
});
```

Which one when? **`aria-pressed`** when the button itself is on or off: a heart, a filter, a dark-mode switch. **`aria-expanded`** when the button opens and closes something else: a recipe's steps, a menu, a "more details" panel.

## Escape closes it, and focus goes back

Keyboard users expect **Escape** to close whatever just opened: a recipe's steps, a menu, a photo shown full-screen. And when something closes, focus matters. If the element that has focus is hidden, focus falls back to the start of the page, and a keyboard user has to Tab all the way back to where they were. So after closing, put focus back on the button that opened it, with **`focus()`**:

```javascript
document.addEventListener('keydown', (event) => {
  if (event.key !== 'Escape' || showSteps.getAttribute('aria-expanded') !== 'true') {
    return;
  }
  showSteps.setAttribute('aria-expanded', 'false');
  steps.hidden = true;
  showSteps.focus();
});
```

`!==` is read "not strictly equal". The first `if` ends the listener early unless the key is Escape *and* the steps are open. Then it closes them the same way a click does, and `showSteps.focus()` moves the focus ring back to the button. In the Console, `document.activeElement` tells you which element has focus right now.

**Try it.** Add the **Show steps** button, the steps and both listeners to your `weeknight` page. Put the mouse aside: Tab to **Show steps** and press the space bar to open the steps. Press Shift+Tab once, so the focus ring moves back to the control before it, then press Escape. The steps close, and the focus ring jumps back to **Show steps**. Comment out the `showSteps.focus()` line and try again: this time the focus ring stays where it was.

## Leave the keys alone while someone types

A `keydown` listener on the whole page hears every key, including the ones someone types into a search box. If cook mode's arrow keys moved the recipe steps while a visitor was moving the cursor in the search box, that would be a bug. Skip key presses that start inside a text box:

```javascript
document.addEventListener('keydown', (event) => {
  if (event.target.closest('input, textarea')) {
    return;
  }
  console.log('Cook mode heard', event.key);
});
```

`event.target` is the element with focus, and `closest('input, textarea')` finds it when it's a text box (the comma in a selector means "or"). The arrow keys now move the cursor in the search box, as they should, and move through the steps everywhere else.

## Check yourself

2 questions to try on the lesson page.

## So what

A real `<button>`, ARIA attributes that tell the truth, and an Escape key that hands focus back: with those, the same page works for a mouse, a keyboard and a screen reader, and you'll write all three habits into every interactive page from here on.

## Key terms

- [Accessibility](https://www.ask-codey.com/glossary/accessibility)
- [&lt;button>](https://www.ask-codey.com/glossary/button)
- [Sequential focus navigation](https://www.ask-codey.com/glossary/sequential-focus-navigation)
- Screen reader
- WCAG

## Cheat sheets

- [Keyboard Users and Accessible Controls](https://www.ask-codey.com/cheat-sheets/javascript-events-keyboard-and-accessibility-cheat-sheet)

## Related

- [The Event Object](https://www.ask-codey.com/lessons/javascript-events-the-event-object)
- [Bubbling and Event Delegation](https://www.ask-codey.com/lessons/javascript-events-bubbling-and-delegation)

Source: https://www.ask-codey.com/lessons/javascript-events-keyboard-and-accessibility
