# Commit on the Up-Event, Not the Down-Event

**MUST** · **ID:** `interactions-pointer-cancellation` · **Category:** interactions
**Source:** [WCAG](https://www.w3.org/WAI/WCAG21/quickref/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/interactions-pointer-cancellation

> MUST: Preview on down, COMMIT on up: the down-event may arm, highlight, reveal, or open a menu (all reversible — that is why opening dropdowns on `mousedown` is fine), but destructive/irreversible functions (delete, submit, purchase, send, publish) must complete on the up-event on the same element, so sliding off before release aborts them. WCAG SC 2.5.2 Pointer Cancellation, Level A — `click` gives you this for free; if you truly must act on down, ship an undo instead.

Let a mistaken press be aborted by sliding off before release; complete actions on pointerup

Firing an action on pointerdown removes the one escape hatch every pointer user relies on: land on the wrong control, slide off, release, nothing happens. Take that away and a mis-aimed tap on a phone, or a tremor that lands a finger 4px off, is instantly irreversible. Draw the line carefully, because this corpus also tells you to open dropdowns on mousedown, and that is not a contradiction: opening a menu does not EXECUTE anything and is trivially reversible, so the down-event is free to preview, arm, highlight, or reveal. What it must not do is commit. Delete, submit, purchase, send, publish — these complete on the up-event, on the same element the press started on, which is exactly what the platform gives you for free in the click event. So the rule reads: preview on down, commit on up. If you genuinely must act on down (a piano key, a game control — the SC calls this "essential"), the alternative branch of the SC requires you to provide an undo instead.

## Bad — do not do this

`interactions-pointer-cancellation-bad`

```tsx
import { useState } from 'react';

const INITIAL = ['Q3 forecast.xlsx', 'Offsite photos', 'contract-final.pdf'];

export function PointerCancellationBad() {
  const [files, setFiles] = useState(INITIAL);
  const [log, setLog] = useState<string | null>(null);

  return (
    <div className="w-full max-w-sm">
      <ul className="space-y-2 rounded-lg border border-border bg-card p-3">
        {files.map((file) => (
          <li
            key={file}
            className="flex items-center justify-between gap-3 rounded border border-border bg-muted px-3 py-2"
          >
            <span className="text-sm text-foreground">{file}</span>

            {/* The destructive action executes on the DOWN-event. By the time the
                finger has landed, the file is already gone. Sliding off before
                release cannot save you, because release is never consulted. */}
            <button
              type="button"
              onPointerDown={() => {
                setFiles((prev) => prev.filter((f) => f !== file));
                setLog(`Deleted "${file}" on pointerdown`);
              }}
              className="rounded bg-destructive px-2 py-1 text-xs text-destructive-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
            >
              Delete
            </button>
          </li>
        ))}
        {files.length === 0 && (
          <li className="px-3 py-2 text-sm text-muted-foreground">No files left.</li>
        )}
      </ul>

      <div className="mt-3 flex items-center justify-between gap-3">
        <p className="text-xs text-destructive">{log ?? 'Nothing deleted yet.'}</p>
        <button
          type="button"
          onClick={() => {
            setFiles(INITIAL);
            setLog(null);
          }}
          className="shrink-0 rounded border border-border px-2 py-1 text-xs text-foreground hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        >
          Reset
        </button>
      </div>

      <p className="mt-3 text-xs text-muted-foreground">
        Press <em>Delete</em> and — without releasing — drag your pointer far away, then
        let go. The file is already deleted. There was never a moment to change your
        mind. For someone with a tremor, or anyone whose finger lands slightly off on a
        phone, the down-event is the whole interaction.
      </p>

      <p className="mt-2 text-xs text-destructive">
        The down-event executes the function, so the press cannot be aborted — WCAG 2.5.2 failure
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-pointer-cancellation-good`

```tsx
import { useState } from 'react';

const INITIAL = ['Q3 forecast.xlsx', 'Offsite photos', 'contract-final.pdf'];

export function PointerCancellationGood() {
  const [files, setFiles] = useState(INITIAL);
  const [pressing, setPressing] = useState<string | null>(null);
  const [log, setLog] = useState<string | null>(null);

  return (
    <div className="w-full max-w-sm">
      <ul className="space-y-2 rounded-lg border border-border bg-card p-3">
        {files.map((file) => (
          <li
            key={file}
            className="flex items-center justify-between gap-3 rounded border border-border bg-muted px-3 py-2"
          >
            <span className="text-sm text-foreground">{file}</span>

            {/* The down-event only PREVIEWS: it arms the button and shows the hint.
                onClick is the up-event on the same target, so releasing anywhere
                else silently aborts — the browser's own cancellation mechanism. */}
            <button
              type="button"
              onPointerDown={() => {
                setPressing(file);
                setLog(null);
              }}
              onPointerUp={() => setPressing(null)}
              onPointerLeave={() => {
                if (pressing === file) {
                  setPressing(null);
                  setLog(`Aborted — released "${file}" off-target, nothing deleted`);
                }
              }}
              onPointerCancel={() => setPressing(null)}
              onClick={() => {
                setFiles((prev) => prev.filter((f) => f !== file));
                setLog(`Deleted "${file}" on release`);
              }}
              className={`rounded px-2 py-1 text-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring ${
                pressing === file
                  ? 'bg-destructive text-destructive-foreground'
                  : 'border border-border text-destructive'
              }`}
            >
              Delete
            </button>
          </li>
        ))}
        {files.length === 0 && (
          <li className="px-3 py-2 text-sm text-muted-foreground">No files left.</li>
        )}
      </ul>

      <div className="mt-3 flex items-center justify-between gap-3">
        <p className="text-xs text-foreground">
          {pressing
            ? `Armed — release on "${pressing}" to confirm, slide off to cancel`
            : (log ?? 'Nothing deleted yet.')}
        </p>
        <button
          type="button"
          onClick={() => {
            setFiles(INITIAL);
            setLog(null);
          }}
          className="shrink-0 rounded border border-border px-2 py-1 text-xs text-foreground hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        >
          Reset
        </button>
      </div>

      <p className="mt-3 text-xs text-muted-foreground">
        Press <em>Delete</em> and hold — the button arms and tells you how to escape.
        Slide off before releasing and nothing happens: <code>click</code> only fires
        when the up-event lands on the same element as the down-event. Release on the
        button and the file goes. This is opening-a-menu-on-mousedown&apos;s sibling
        rule, not its opposite: preview on down, <em>commit</em> on up.
      </p>

      <p className="mt-2 text-xs text-success">
        The up-event completes the function, so any press can still be aborted
      </p>
    </div>
  );
}
```

## References

- [WCAG 2.1: Understanding SC 2.5.2 Pointer Cancellation](https://www.w3.org/WAI/WCAG21/Understanding/pointer-cancellation.html)
- [MDN: Element click event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event)
