# Buttons that act on press and can't be cancelled

Source: https://easeweb.dev/learn/pointer-cancellation
Topics: Buttons and links > Actions run on release, not on press (WCAG 2.5.2)
The fix: Run the action on the click event of a native button, and delete the pointerdown handler. A click fires only when the press and the release both happen on the button, so sliding off cancels it, a scroll never triggers it, and Enter and Space work with no extra code.
Test with: mouse, touch, keyboard, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F101 https://www.w3.org/WAI/WCAG22/Techniques/general/G212 https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event

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

On almost every button people use, touching it is not yet a decision; letting go is. In between, anyone who touched the wrong thing can slide off and change their mind, and most people do it without noticing. A shop lists its products in cards, each with an Add to basket button. To make the button feel faster, it adds the item the moment it is pressed, not when it is released. Press it, realise it's the wrong towel and slide off before letting go: the basket count has already gone up. On a phone it is worse. A finger that starts a scroll on the button adds the item as the page starts to move, so scrolling down a list of products quietly fills the basket.

## Why it fails

WCAG 2.5.2 Pointer Cancellation asks that a single pointer press can be taken back. The function must not run on the down-event at all, or it runs on the up-event with a way to abort it before release or undo it after, or the release reverses what the press did, or running on the press is essential, as on a piano keyboard. Here the whole action runs on `pointerdown`, the down-event, and nothing about it is essential. An undo button would not rescue it either: the criterion's undo route applies only to an action that completes on the up-event. W3C lists this exact pattern as failure F101, activating a control on the down-event.

## Who is affected

Anyone scrolling a long product list with a thumb meets the same failure, and it spreads to every touch screen at once: a phone, a tablet or a touch laptop all fire the press as the finger lands.

## What automation and AI miss

The button has a name, a role and a focus ring, so every markup check passes. A scripted test calls `click()` or presses and releases at the same point in a single step, and both work on the broken button. Nothing that runs the happy path ever slips. A scanner can't see which event a script listens for, since frameworks often attach listeners away from the element, and a down listener is not a failure by itself: many buttons use one to draw a ripple. AI tools often suggest `onMouseDown` or `onPointerDown` to make a control feel faster, or to stop a text field losing focus first, and the result works perfectly in a demo with a steady hand.

## Before: the action on pointerdown

```html
<button type="button" class="add">Add to basket</button>
```

```js
// Add on press, so the button feels faster.
addButton.addEventListener('pointerdown', addToBasket);
// pointerdown never comes from the keyboard, so Enter and Space are wired separately.
addButton.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' || event.key === ' ') addToBasket();
});
```

`pointerdown` fires for a mouse, a pen and a finger the instant they touch the button, so the item is added before the person can see what they pressed or move away. On touch it also fires at the start of every scroll that begins on the button. `mousedown`, `touchstart` and a framework's `onMouseDown` or `onTouchStart` fail the same way.

## After: the action on click

```html
<button type="button" class="add">Add to basket</button>
```

```js
addButton.addEventListener('click', addToBasket);
```

Delete both listeners and use the button's own `click`. The browser fires `click` only when the press and the release both happen on the button, so sliding off before letting go cancels it, and a touch that turns into a scroll never becomes a click. The same event fires for Enter and Space on a focused button, for a screen reader's double tap and for voice control, so the separate keyboard code goes too. The click on a button that has already been released feels just as fast: the time between press and release is what the person spends deciding.

## Implementation decisions

A down listener is fine as long as it never completes the action. These uses are common and pass:

- A pressed look: `:active` in CSS shows it with no script, and a ripple drawn on `pointerdown` is fine while the action waits for `click`.
- Keeping focus: a suggestion list chosen with `mousedown` usually does it so the search field doesn't lose focus and close the list first. Keep the `mousedown`, but only to call `event.preventDefault()`, which stops the focus moving, and choose the option on `click`. [Autocomplete fields with silent, mouse-only suggestions](/learn/combobox) covers the rest of that list.
- Starting a drag: the drop happens on release, and dropping outside any target must put the item back. [Drag-and-drop boards that only a mouse can use](/learn/drag-and-drop) covers the other ways to move it.
- Press and hold: a preview that appears while held and disappears on release passes, because the release reverses the press. A hold that commits something, such as "hold to delete", must still finish on release and cancel when the pointer leaves.

Some things that look like fixes are not. `pointerup`, `mouseup` and `touchend` are up-events, but they don't cancel. In Chromium, `touchend` fires on the button even after the finger has scrolled the page, and with a mouse, `pointerup` fires on a button the pointer arrives at after pressing somewhere else. `setPointerCapture()` on a plain button routes the release back to it, and the browser then fires `click` on the button even when the pointer was released far away, so leave capture to things that are really dragged.

Component libraries often wrap this in a press event. Use the one that fires on release over the control, such as `onPress`, not a "press start" event, and try the slide-off test on it rather than trusting the name. A link needs nothing: following a link is already a click. For an action with lasting effects, such as deleting or paying, release alone is not much protection, so add an undo or a confirmation as well.

## Verify the fix

1. Search the code for `pointerdown`, `mousedown`, `touchstart` and the framework's `onMouseDown`, `onPointerDown` and `onTouchStart`. Each one only shows a pressed look, keeps focus or starts a drag.
2. With a mouse, press the button, move well off it and release. Nothing happens. Then press and release on it: the action runs once.
3. On a real phone, start a scroll with your finger on the button. The page scrolls and the action does not run.
4. Click and tap the button normally. The action runs once each time.
5. Tab to the button and press Enter, then Space. The action runs once for each.

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

## Limits

This is a starting pattern, not a guarantee for every application. Some controls must act on the press because that is what they are, such as the keys of an on-screen keyboard or a musical instrument; WCAG treats those as essential. A small button can still be missed in the first place: [tap targets too small and too close together](/learn/target-size) covers its size. Test the complete journey on real touch devices as well as with a mouse.
