# Hold-to-confirm buttons and other timed key presses

Source: https://easeweb.dev/learn/timed-key-presses
Topics: Keyboard and focus > Keys work without timed or held presses (WCAG 2.1.1)
The fix: Make one ordinary press do the job. Either the press opens a confirmation that waits for an answer, or it deletes at once and leaves an Undo in the list's place for as long as the page is open. A hold or a quick double press may stay as an extra, never as the only way.
Test with: keyboard, speech input, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Techniques/general/G202 https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

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.

## What happens

A shop's account page lists the customer's saved lists, such as Kitchen ideas with 14 items, each with a way to delete it. The designer wanted deleting to take a moment's thought without a dialog in the way, so the button says "Hold to delete": keep it pressed, with the mouse, a finger, Enter or Space, and a fill sweeps across it; let go before 1.5 seconds and it drains away. A second version of the same idea is a plain "Delete list" button that changes to "Press again to delete" after the first press, and changes back after 3 seconds.

Both work with a keyboard, as long as the hands and the keyboard can keep time. A switch user's press, a spoken "press Enter" and a tap on an on-screen keyboard each arrive as one quick press and release: the fill starts and drains in the same moment, and the list stays. Pressing again in time means reaching the button again within 3 seconds, which a switch scan or a spoken command may not manage.

## Why it fails

Success criterion 2.1.1 asks for every function to work from a keyboard "without requiring specific timings for individual keystrokes". The Understanding document gives two examples of such timings: keystrokes that have to be repeated or made within a short period of time, and a key that must be held down for an extended period before the keystroke registers. The hold button is the second, and the "press again" button is the first. Both handle the keyboard, and both still fail: the only way to delete is to press in time.

Making the times kinder doesn't change the result. A shorter hold or a longer window is still a timing someone can miss. The way to pass is to remove the clock from the keystroke.

## Who is affected

The failure rarely shows up in testing, because the people who build and test the page press keys the way the timer expects. It shows up as a customer who "can't delete anything" and an account full of lists they meant to remove.

## What automation and AI miss

The control is a native button with a visible name, so a scan finds nothing wrong, and a script that presses Enter sees a button that does nothing, which it can't tell apart from a button that is waiting for something else. Pressing a delete button on a live site is not a test an audit can run anyway. The EaseWeb audit reads the words on the page instead: "Hold to", "press and hold", or "press again to" near a control is sent to review. A hold with no words, or a "press again" that appears only after the first press, is found by nothing. AI coding tools will build hold-to-confirm buttons when asked, with keydown and keyup handlers so they work with a keyboard, and may stop there: the timing half of 2.1.1 is the half that is easy to forget.

## Before: a button that deletes only while held

```html
<h2>Saved lists</h2>
<ul>
  <li>
    <h3>Kitchen ideas</h3>
    <p>14 items</p>
    <button type="button" id="delete-list" class="hold">
      <span class="hold-fill" aria-hidden="true"></span>
      Hold to delete
    </button>
  </li>
  …
</ul>
```

```js
const button = document.getElementById('delete-list');
let timer;
function startHold(event) {
  if (event.type === 'keydown' && (event.repeat || (event.key !== 'Enter' && event.key !== ' '))) return;
  event.preventDefault();
  button.classList.add('holding'); // the fill sweeps across in 1.5 seconds
  timer = setTimeout(deleteList, 1500);
}
function stopHold() {
  clearTimeout(timer);
  button.classList.remove('holding');
}
button.addEventListener('pointerdown', startHold);
button.addEventListener('keydown', startHold);
button.addEventListener('pointerup', stopHold);
button.addEventListener('pointerleave', stopHold);
button.addEventListener('keyup', stopHold);
```

The only way to delete is to keep a key or a pointer down for 1.5 seconds. One quick press starts the timer and stops it again straight away, and a screen reader's click in browse mode is over just as quickly, or never starts it at all.

## Before: a button that must be pressed again within 3 seconds

```html
<h2>Saved lists</h2>
<ul>
  <li>
    <h3>Kitchen ideas</h3>
    <p>14 items</p>
    <button type="button" id="delete-list">Delete list</button>
  </li>
  …
</ul>
```

```js
const button = document.getElementById('delete-list');
let armed = false;
let timer;
button.addEventListener('click', () => {
  if (armed) {
    clearTimeout(timer);
    deleteList();
    return;
  }
  armed = true;
  button.textContent = 'Press again to delete';
  timer = setTimeout(() => {
    armed = false;
    button.textContent = 'Delete list';
  }, 3000);
});
```

Each press is an ordinary one, but the second has to come within 3 seconds of the first, or the button forgets the first. That window is the timing 2.1.1 rules out.

## After: one press opens a question that waits

```html
<h2 id="lists-heading" tabindex="-1">Saved lists</h2>
<ul>
  <li id="list-kitchen">
    <h3>Kitchen ideas</h3>
    <p>14 items</p>
    <button type="button" id="delete-list">Delete list</button>
  </li>
  …
</ul>

<dialog id="confirm-delete" role="alertdialog"
  aria-labelledby="confirm-title" aria-describedby="confirm-message">
  <form method="dialog">
    <h2 id="confirm-title">Delete “Kitchen ideas”?</h2>
    <p id="confirm-message">The list and its 14 saved items will be deleted.</p>
    <button value="keep" autofocus>Keep list</button>
    <button value="delete">Delete list</button>
  </form>
</dialog>
```

```js
const dialog = document.getElementById('confirm-delete');
document.getElementById('delete-list').addEventListener('click', () => {
  dialog.returnValue = '';
  dialog.showModal();
});
dialog.addEventListener('close', async () => {
  // Keep list and Escape: the browser returns focus to Delete list.
  if (dialog.returnValue !== 'delete') return;
  await deleteList();
  document.getElementById('list-kitchen').remove();
  document.getElementById('lists-heading').focus();
});
```

The moment of thought the hold was for is now a question, and it waits as long as the person needs. One click, tap, Enter or Space opens it, and one more press answers it. Focus starts on Keep list, so a press that repeats keeps the list, and once the list is gone, focus moves to the heading above the lists instead of falling to the page's body. How to make the question itself read out is covered in [confirmation dialogs that don't read out their question](/learn/confirmation-dialogs).

## After: delete at once, with an Undo that stays

```html
<h2>Saved lists</h2>
<ul>
  <li id="list-kitchen">
    <div class="list-body">
      <h3>Kitchen ideas</h3>
      <p>14 items</p>
      <button type="button" class="delete">Delete list</button>
    </div>
    <p class="deleted" tabindex="-1" hidden>
      “Kitchen ideas” deleted.
      <button type="button" class="undo">Undo</button>
    </p>
  </li>
  …
</ul>
```

```js
const card = document.getElementById('list-kitchen');
const body = card.querySelector('.list-body');
const deleted = card.querySelector('.deleted');
card.querySelector('.delete').addEventListener('click', async () => {
  await deleteList(); // the server keeps the list in a bin, so it can come back
  body.hidden = true;
  deleted.hidden = false;
  deleted.focus();
});
card.querySelector('.undo').addEventListener('click', async () => {
  await restoreList();
  deleted.hidden = true;
  body.hidden = false;
  card.querySelector('.delete').focus();
});
```

One press deletes, and the safety the hold was for comes after the press instead of before it. The message takes the list's place and takes focus, so a screen reader reads "“Kitchen ideas” deleted. Undo", and Undo is the next Tab stop. It stays until the person leaves the page: Undo in a toast that fades would bring a time limit back, as [toasts that vanish before you can read or undo them](/learn/toast-timing) explains. This needs the server to keep a deleted list for a while, in a bin or as a soft delete, so that `restoreList()` has something to restore.

## Which option to use

Both pass 2.1.1, and neither asks anyone to keep time. The question costs one more press on every delete, but it needs nothing from the server, and it stops a mistake before it happens: choose it when what is deleted can't come back. Undo keeps deleting to a single press, which suits people who tidy up often and anyone for whom every press is effort, but it only works when the server can restore the list: choose it when deleting is frequent and reversible. Whichever you choose, a press and hold with a mouse or a finger can stay as a quicker way in for people who like it, as long as a click, a tap, Enter and Space do the whole job without it. Let that hold finish when the finger or button is lifted, and cancel when it slides off, as [buttons that act on press and can't be cancelled](/learn/pointer-cancellation) explains.

## Implementation decisions

Use a native `button` for the control, so Enter and Space work without a key handler of your own, and give it its own focus ring: the fill that sweeps across a hold button is not a focus indicator. A control built from a `div` fails 2.1.1 in a different way, covered in [div buttons that can't be reached with Tab](/learn/native-buttons), and a missing ring in [missing focus indicators on custom buttons](/learn/visible-focus).

Don't fix the Before by changing its numbers. A 0.5 second hold still has to be held, and a 10 second window still closes. Count every timer that starts on keydown, pointerdown or a first click, and make sure none of them stands between a person and an action.

The same failure comes in other shapes than delete buttons, and each has a single-press way:

- Hold to record a voice message: press once to start and once to stop, with a Stop button that shows the time recorded.
- Hold Space to talk in a call: a Mute button that toggles, with the held Space kept as an extra for people who like it. A Space that acts anywhere on the page, not only on a focused control, gets in the way of speech input and typing the same way [single-key shortcuts that can't be turned off](/learn/character-key-shortcuts) do, so let people turn it off too.
- Hold an eye icon or Shift to show a password: a Show password button that stays on until it is pressed again, with `aria-pressed` saying which state it is in.
- Press Escape twice to close an editor: one Escape closes it, and if unsaved work would be lost, a question that waits asks first.

Key combinations are not timed presses. Ctrl + K is pressed as one chord, and Sticky Keys lets people press it one key at a time; nothing is held for a set time. Session time-outs and other limits on a whole task are a different criterion, 2.2.1 Timing Adjustable, not this one.

## Verify the fix

1. Put the mouse aside, Tab to the delete control and press Enter once, quickly, then do the same with Space: the question opens, or the list is deleted and its message takes focus.
2. Leave the question open, or the deleted message showing, for at least 30 seconds: nothing closes, resets or changes back by itself, and Keep list, Delete list and Undo all still work.
3. With a screen reader in browse mode, press Enter on the delete control: the question and its message are read when it opens, or "“Kitchen ideas” deleted" is read after it.
4. With a switch, an on-screen keyboard or speech input ("press Enter", "click Delete list"), delete a list and bring it back or keep it, at the tool's own pace.
5. After deleting, keeping or undoing, check where focus is: on Delete list, the deleted message or the lists' heading, never on the page's body.

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

## Limits

This is a starting pattern, not a guarantee for every application. The snippets leave out the server calls and restoring a list to its place among several. A press and hold made with a mouse or a finger, and long presses that open menus, are covered from the pointer's side in [right-click, long-press and swipe actions with no button](/learn/context-menus). Test the complete journey with a keyboard, a switch, speech input and your target assistive technology.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
