npx skills add ...
npx skills add mgifford/accessibility-skills --skill touch-pointer
Load this skill whenever the project contains interactive UI elements that users touch, tap, click, drag, swipe, or gesture with — buttons, links, drag-and-drop interfaces, sliders, carousels, or custom touch interactions. Under no circumstances create touch targets smaller than 44×44 CSS pixels. Absolutely always provide pointer cancellation, single-pointer alternatives to gestures, and load alongside keyboard/SKILL.md since pointer and keyboard requirements are complementary.
npx skills add mgifford/accessibility-skills --skill touch-pointer
Canonical source:
examples/TOUCH_POINTER_ACCESSIBILITY_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when implementing any interactive UI that users touch, click,
tap, drag, or gesture with.
Load alongside keyboard/SKILL.md — pointer and keyboard requirements are complementary, not interchangeable.
Build interfaces that work with touchscreens, mice, trackpads, pens, head pointers, switch-controlled pointers, and other pointing devices. Do not infer a person's abilities from the device they use — a touchscreen user may also use a keyboard, speech input, a screen reader, or a mouse.
A keyboard-only alternative does not by itself satisfy pointer-gesture (2.5.1) or dragging (2.5.7) requirements — the alternative must work with a single pointer, without a path-based gesture or drag, even though keyboard operability is separately required by 2.1.1. Native buttons conveniently satisfy both at once.
| Level | Meaning |
|---|---|
| Critical | Functionality only available via multi-point gesture with no single-pointer alternative; user-scalable=no prevents zoom |
| Serious | Drag-to-reorder with no single-pointer alternative; touch target under 24×24px for primary actions |
| Moderate | Mousedown/touchdown action with no up-event cancellation; motion gesture without UI alternative |
| Minor | Target under 44×44px for non-primary actions; spacing between targets too small |
Prioritize by actual user impact, task criticality, reach, and frequency — don't assign severity solely from a success-criterion number or tool output.
Use native HTML controls and links — their built-in click activation works
across mouse, touch, pen, keyboard, and many AT.
Use Pointer Events for custom direct-manipulation components rather than
maintaining separate mouse/touch implementations. Do not gate core
functionality on pointerType — a pen user shouldn't lose a function a mouse user gets.
user-scalable=no)Some libraries/frameworks add user-scalable=no automatically — audit the
viewport meta tag on every project. Do not rely on browsers ignoring
restrictive viewport settings; remove the restriction from the source. Test
text at 200% and layout at 400% zoom.
A multipoint gesture uses two or more contact points (pinch zoom). A path-based gesture depends on the route/direction/shape traced, not just start/end points. When content defines such a gesture, provide a way to perform the same function with a single pointer and no path-based gesture — the alternative must exist in the content, not just as a keyboard command.
Examples: buttons for zooming/panning a map; previous/next buttons for a swipe carousel; a menu command alongside drawing a shape; visible rotate/ resize controls. Gestures required by the browser or AT itself (a mobile screen reader's navigation gestures) are outside the author's control — don't reproduce or interfere with them.
If a function uses dragging, provide a way to complete it with a single pointer without dragging — a keyboard-only alternative does not by itself satisfy 2.5.7. Native buttons satisfy both the pointer requirement and keyboard access at once.
Drag-and-drop may remain as an enhancement. The non-dragging controls must expose the same result, remain visible/discoverable, and preserve focus and state after the move. Other patterns: select item + destination + activate Move; tap a map control to pan; enter a value or click the track alongside dragging a slider thumb.
Activate ordinary controls on click, not pointerdown/mousedown/touchstart.
WCAG permits several patterns: (1) the down event doesn't execute the function; (2) the function completes on the up event and the user can abort before releasing or undo after; (3) releasing reverses the down event's outcome; (4) completing on down is essential. For destructive or hard-to-reverse actions, also confirm or provide Undo:
Pointer targets must be at least 24×24 CSS pixels, except when:
44×44 CSS pixels is the recommended default for important/frequent controls — easier to implement and test than relying on the spacing exception, and it's also the WCAG 2.5.5 (AAA) Target Size Enhanced threshold.
Use rem so the target grows with the user's default text size. Put
padding on the interactive element itself, not a non-interactive wrapper —
the whole padded area must be clickable:
Do not apply a blanket minimum width/height to every <a> — inline prose
links have a defined exception and forcing them into square boxes damages
reading and wrapping. Associate checkboxes/radios with visible <label>
elements so the label extends the operable area.
If shaking, tilting, or gesturing toward a camera performs a function: provide a conventional UI control for the same function; let the user disable motion actuation; request sensor permission only in response to a clear user action.
Keep the conventional button available whether motion input is enabled or not — default the sensor feature to off.
Every function revealed on hover must also be available through a persistent control or keyboard focus — touch devices may not support hover reliably.
Content on hover/focus must be dismissible, hoverable, and persistent (WCAG 1.4.13) unless an exception applies. A visible button/disclosure is usually more reliable than a tooltip for essential instructions.
pointer/hover Media Features Carefullypointer/hover describe the primary device; any-pointer/any-hover
report capabilities across all available devices. These do not detect
keyboard use, identify a disability, or reliably classify every pen as
coarse or every touchscreen as primary — hybrid devices can report several
capabilities that change while the page is open.
Never hide essential controls behind an accurate-pointer query:
Start with an accessible base layout, then use media queries only for enhancements.
Track the active pointerId; finish on pointerup; cancel cleanly on
pointercancel; account for lostpointercapture; don't assume every pointer
reports pressure/tilt/dimensions; still provide non-gesture, non-dragging
controls for the same function. Do not register both touchend and click
for the same action without preventing duplicate activation.
touch-actionUse touch-action only on the smallest custom surface that needs it. Never
broadly apply touch-action: none to html, body, page containers, maps,
canvases, or carousels — it suppresses browser panning/zooming starting on
that element. Before restricting a gesture, confirm: the component genuinely
needs to handle it; page scrolling still works from the component; pinch
zoom remains available; a simple control provides the same function.
touch-action is preferable to non-passive preventDefault() on every touch move.
| Component | Required approach |
|---|---|
| Carousel | Prev/next/pause controls; don't require swiping; preserve scrolling and reduced-motion |
| Map | Named zoom/pan controls; address/coordinates/list alternative when the visual map alone isn't sufficient |
| Slider | Prefer <input type="range">; provide numeric input or click-the-track alongside thumb dragging |
| Sortable list | Move up/down/to controls in addition to drag-and-drop; announce result, preserve focus |
| Swipe action | Expose via a visible button/menu; swipe must not be the only way to delete/archive/reveal |
| Long press | Provide an ordinary button/menu; must not be the only route to a function |
| Drawing/signature | Avoid requiring fine path accuracy when not essential; offer typed/uploaded alternatives where the task allows |
| Canvas control | Provide an accessible DOM interface for all operations, names, values, and results — canvas pixels alone create no semantics |
Do not depend on double-tap, pressure, tilt, edge swipes, or device-specific gestures for essential functions.
Speech-input users (Dragon, iOS Voice Control) often activate a control by saying its visible label — the accessible name must contain that text.
Putting the visible label at the start of a longer accessible name is a useful convention for speech input. An icon-only button has no visible label to compare under 2.5.3, but still needs a clear accessible name under 4.1.2 — use familiar icons and a visible label where space allows; a tooltip is not a substitute for a reliably available label.
Support portrait and landscape unless one orientation is essential — don't use orientation locks to compensate for an inflexible layout. At narrow widths and high zoom: keep controls in a meaningful order; wrap toolbars instead of shrinking targets below minimum size; prevent sticky headers/banners/chat widgets from obscuring focused controls; keep dialogs/ menus within the viewport; avoid horizontal page scrolling except where genuinely 2D content requires it. When an on-screen keyboard opens, focused inputs/errors/submit controls must remain visible or scrollable into view.
Mobile screen readers change how touch input reaches the page — users may explore by touch, swipe through the accessibility tree, and use a screen-reader activation gesture instead of directly tapping. Use native elements and accurate accessible names; keep the accessible target aligned with the visible control; expose current state (expanded/selected/checked/ value); avoid custom gestures conflicting with AT gestures; don't require spatial knowledge ("tap the shape in the upper-right corner"). Test both direct touch with the screen reader off and touch exploration with it on — passing one doesn't establish the other works.
Touch input can be imprecise or interrupted. For consequential actions: separate adjacent destructive/constructive actions; use clear labels instead of ambiguous icons; don't execute on pointer-down; confirm hard-to-reverse actions; offer Undo when practical; preserve entered data after validation errors, orientation changes, or temporary disconnection. Target size is not the only safeguard — a large Delete button next to Save with no recovery path can still cause harm.
touch-action rules; verify
visible labels are in accessible namesAutomated checks can identify some small targets, invalid names, restrictive viewport settings, and duplicate event patterns — cannot determine whether a gesture is essential, whether an alternative is equivalent/discoverable, or whether touch screen-reader interaction is understandable. Manual testing remains required.
| Failure | Correction |
|---|---|
| Browser zoom disabled because the layout breaks | Fix the responsive layout; remove viewport scaling restrictions |
| A keyboard command is the only alternative to a pinch/path gesture | Add controls that work with a single pointer without a path gesture |
| Keyboard reordering offered as the only drag alternative | Add single-pointer Move controls that don't require dragging |
Action fires on pointerdown for perceived responsiveness | Complete on click/pointerup, with cancellation or Undo |
| "Every target requires 24×24px, no exceptions" | Apply the exact 2.5.8 rule and its defined exceptions |
| Vague spacing claimed to make an undersized target conform | Evaluate the actual 24px circle test |
| Every link forced into a square target | Leave inline prose links in normal flow |
| Padding placed on a wrapper around a small icon button | Put padding on the interactive element itself |
| Controls appear only on hover | Provide persistent or focus-triggered controls, touch-operable disclosure |
pointer: coarse treated as a reliable touchscreen/stylus detector | Treat media features as capabilities; keep the base experience input-agnostic |
Essential controls hidden unless pointer: fine matches | Keep core controls in the base layout |
| Separate touch and mouse handlers double-activate | Use native click or a unified Pointer Events implementation |
touch-action: none applied to a whole page/component | Preserve panning/zooming; restrict only the necessary gesture axis |
| Motion input is always active with no way to disable | Provide a conventional alternative and a user-controlled disable |
| Mobile emulator is the only touch test | Test physical devices, hybrid inputs, and mobile screen readers |
| Passing keyboard tests treated as proof of pointer accessibility | Test gestures, dragging, cancellation, and target size separately |
user-scalable=no and maximum-scale not in viewport meta tagclick, pointerup); destructive actions
confirmable/undoablepointer/hover media queries only enhance an already-accessible base
— never hide essential controlspointercancel/lostpointercapturetouch-action scoped narrowly; page scroll and pinch zoom preserved elsewherearia-label values begin with or contain the visible label text (WCAG 2.5.3)Standards horizon: WCAG 2.5.x criteria are recent additions (2.1 and 2.2). WCAG 3.0 is expected to extend pointer and touch requirements further. Monitor: https://www.w3.org/TR/wcag-3.0/
<!-- Critical violation — never do this -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- Correct -->
<meta name="viewport" content="width=device-width, initial-scale=1"><div class="map-controls" aria-label="Map controls">
<button type="button" data-map-action="zoom-in">Zoom in</button>
<button type="button" data-map-action="zoom-out">Zoom out</button>
<button type="button" data-map-action="north">Pan north</button>
</div><ul id="task-list">
<li>
<span>Review content</span>
<button type="button" data-move="up" aria-label="Move up: Review content">Move up</button>
<button type="button" data-move="down" aria-label="Move down: Review content">Move down</button>
</li>
</ul>
<p id="reorder-status" role="status" aria-atomic="true"></p>taskList.addEventListener("click", (event) => {
const button = event.target.closest("button[data-move]");
if (!button) return;
const item = button.closest("li");
const label = item.querySelector("span").textContent;
const direction = button.dataset.move;
if (direction === "up" && item.previousElementSibling) item.previousElementSibling.before(item);
else if (direction === "down" && item.nextElementSibling) item.nextElementSibling.after(item);
else return;
updateMoveButtons();
reorderStatus.textContent = `${label} moved ${direction}.`;
});// Right — fires on click; user can drag away to cancel
deleteButton.addEventListener("click", deleteItem);<button type="button" id="archive-button">Archive message</button>
<p id="archive-status" role="status"></p>
<button type="button" id="undo-archive" hidden>Undo archive</button>.icon-button {
display: inline-grid;
min-inline-size: 2.75rem; /* 44px at default 16px root */
min-block-size: 2.75rem;
padding: 0.625rem;
place-items: center;
}
.icon-button svg { inline-size: 1.5rem; block-size: 1.5rem; }<a class="nav-link" href="/account/">Account</a>.nav-link { display: inline-flex; min-block-size: 2.75rem; padding: 0.625rem 0.875rem; align-items: center; }<button type="button" id="undo-button">Undo last change</button>
<label><input type="checkbox" id="shake-toggle"> Enable shake to undo</label><button type="button" aria-expanded="false" aria-controls="account-menu">Account menu</button>
<ul id="account-menu" hidden>…</ul>.toolbar button { min-inline-size: 2.75rem; min-block-size: 2.75rem; }
@media (any-pointer: coarse) { .toolbar { gap: 0.75rem; } }/* Incorrect: touch and keyboard users may lose the controls */
@media (pointer: fine) { .editing-controls { display: flex; } }surface.addEventListener("pointerdown", (event) => {
if (activePointerId !== null) return;
activePointerId = event.pointerId;
surface.setPointerCapture(event.pointerId);
beginPreview(event.clientX, event.clientY);
});
surface.addEventListener("pointerup", (event) => {
if (event.pointerId !== activePointerId) return;
activePointerId = null;
commitInteraction(event.clientX, event.clientY);
});
surface.addEventListener("pointercancel", (event) => {
if (event.pointerId === activePointerId) cancelInteraction();
});
surface.addEventListener("lostpointercapture", () => {
if (activePointerId !== null) cancelInteraction();
});/* A horizontal carousel handles horizontal movement;
preserve vertical page scroll and pinch zoom */
.carousel-viewport { touch-action: pan-y pinch-zoom; }<!-- Good -->
<button type="button">Send</button>
<button type="button" aria-label="Send application">Send</button>
<!-- Incorrect: visible text absent from accessible name -->
<button type="button" aria-label="Submit application">Send</button>