2.5.2 Pointer Cancellation
Level: A | Principle: Operable | Since: WCAG 2.1 | Automation: Manual
What This Means
For actions triggered by a single pointer (click, tap), users must be able to cancel or undo the action. Specifically, actions should fire on the up-event (mouseup, pointerup, touchend) rather than the down-event (mousedown, pointerdown, touchstart). This gives users the ability to move their pointer off the target before releasing to abort the action.
Who This Affects
- Motor impairment users — may accidentally press the wrong target due to tremors or imprecise control
- Cognitive disability users — may click impulsively and need to abort before the action fires
- All users — everyone benefits from being able to move the pointer off a button to cancel
- Touch screen users — fat-finger errors are common on mobile devices
Common Pitfalls
1. Actions firing on mousedown / touchstart
// Bad: action fires immediately on press, no chance to cancel
button.addEventListener('mousedown', () => {
deleteItem();
});
// Bad: same problem with touch
button.addEventListener('touchstart', () => {
submitForm();
});
2. No undo mechanism for destructive actions
Deleting an item with no undo option and no confirmation dialog violates this criterion.
3. Drag-and-drop that commits on press
// Bad: item moves to new position on mousedown
element.addEventListener('mousedown', (e) => {
moveToPosition(e.clientX, e.clientY);
});
4. Custom click handlers that don't allow abort
// Bad: custom handler on pointerdown with no abort path
element.addEventListener('pointerdown', () => {
navigateToPage('/checkout');
});
How to Fix
Use click events or up-events
// Good: click event fires on up-event by default
button.addEventListener('click', () => {
deleteItem();
});
// Good: explicitly use pointerup
button.addEventListener('pointerup', () => {
submitForm();
});
Allow abort by moving off target
The native click event already handles this — if you press a button and drag your mouse off before releasing, the click doesn't fire. Custom implementations must replicate this behavior.
// Good: track down target, only fire if up target matches
let downTarget = null;
element.addEventListener('pointerdown', (e) => {
downTarget = e.target;
});
element.addEventListener('pointerup', (e) => {
if (e.target === downTarget) {
performAction();
}
downTarget = null;
});
Provide undo for completed actions
// Good: show undo option after destructive action
function deleteItem(id) {
const item = removeFromList(id);
showToast('Item deleted', {
action: 'Undo',
onAction: () => restoreItem(item),
duration: 5000,
});
}
Confirmation for irreversible actions
<!-- Good: confirmation before destructive action -->
<button onclick="confirmDelete()">Delete Account</button>
<dialog id="confirm-dialog">
<p>This action cannot be undone. Delete your account?</p>
<button onclick="cancelDelete()">Cancel</button>
<button onclick="proceedDelete()">Delete</button>
</dialog>
How to Test
- Click and hold (mouse down) on several buttons and links, then drag the pointer off the element before releasing. The action should not fire.
- Check that destructive actions (delete, submit, purchase) have either a confirmation step or an undo mechanism.
- Open DevTools and search the JavaScript for
mousedownortouchstartevent listeners that trigger actions immediately on press. - Verify that native
clickevents (which fire on release) are used for most interactive elements. - Pass: Actions fire on pointer release (not press), users can cancel by dragging off the target, and destructive actions have confirmation or undo.
- Fail: Any action fires on mousedown/touchstart without the ability to cancel by moving the pointer off the target.
axe-core Rules
| Rule | What It Checks | |------|---------------| | — | No automated axe-core rule for this criterion. Pointer event behavior requires manual testing. |