Touch and Pointer Accessibility Skill

SkillDev tools

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Touch and Pointer Accessibility Skill skill

What this skill tells your AI

The instructions your AI receives, as published by mgifford/accessibility-skills in skills/touch-pointer/SKILL.md and read by ahel’s review.

Canonical source: examples/TOUCH_POINTER_ACCESSIBILITY_BEST_PRACTICES.md in mgifford/ACCESSIBILITY.md This 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.


Core Mandate

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.


Severity Scale (this skill)

LevelMeaning
CriticalFunctionality only available via multi-point gesture with no single-pointer alternative; user-scalable=no prevents zoom
SeriousDrag-to-reorder with no single-pointer alternative; touch target under 24×24px for primary actions
ModerateMousedown/touchdown action with no up-event cancellation; motion gesture without UI alternative
MinorTarget 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.


Start With an Input-Agnostic Base

Use native HTML controls and links — their built-in click activation works across mouse, touch, pen, keyboard, and many AT.

<!-- Incorrect -->
<div class="button" ontouchend="saveChanges()">Save changes</div>

<!-- Correct -->
<button type="button" id="save-button">Save changes</button>
<script>
  document.querySelector("#save-button").addEventListener("click", saveChanges);
</script>

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.


Critical: Never Block Zoom (user-scalable=no)

<!-- 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">

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.


Critical: Single-Pointer Alternatives for Multi-Point/Path Gestures (WCAG 2.5.1)

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.

<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>

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.


Critical: Non-Dragging Alternatives (WCAG 2.5.7 — WCAG 2.2)

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.

<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}.`;
});

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.


Serious: Pointer Cancellation (WCAG 2.5.2)

Activate ordinary controls on click, not pointerdown/mousedown/touchstart.

// Right — fires on click; user can drag away to cancel
deleteButton.addEventListener("click", deleteItem);

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:

<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>

Serious: Target Size Minimum (WCAG 2.5.8 — WCAG 2.2)

Pointer targets must be at least 24×24 CSS pixels, except when:

  • Spacing — a 24px-diameter circle centered on the target doesn't intersect another target's equivalent circle (this is a specific geometric test, not just "add some margin")
  • Equivalent — another control on the same page performs the same function and meets the size requirement
  • Inline — the target is in a sentence, size constrained by line height
  • User agent control — the browser determines the size, unmodified by the author
  • Essential — the specific presentation is essential or legally required

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.

.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; }

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:

<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; }

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.


Serious: Motion Actuation Alternative (WCAG 2.5.4)

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.

<button type="button" id="undo-button">Undo last change</button>
<label><input type="checkbox" id="shake-toggle"> Enable shake to undo</label>

Keep the conventional button available whether motion input is enabled or not — default the sensor feature to off.


Serious: Do Not Depend on Hover

Every function revealed on hover must also be available through a persistent control or keyboard focus — touch devices may not support hover reliably.

<button type="button" aria-expanded="false" aria-controls="account-menu">Account menu</button>
<ul id="account-menu" hidden>…</ul>

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.


Moderate: Use pointer/hover Media Features Carefully

pointer/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.

.toolbar button { min-inline-size: 2.75rem; min-block-size: 2.75rem; }
@media (any-pointer: coarse) { .toolbar { gap: 0.75rem; } }

Never hide essential controls behind an accurate-pointer query:

/* Incorrect: touch and keyboard users may lose the controls */
@media (pointer: fine) { .editing-controls { display: flex; } }

Start with an accessible base layout, then use media queries only for enhancements.


Moderate: Prefer Pointer Events for Custom Interaction

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();
});

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.


Moderate: Preserve Browser Panning/Zooming With touch-action

/* A horizontal carousel handles horizontal movement;
   preserve vertical page scroll and pinch zoom */
.carousel-viewport { touch-action: pan-y pinch-zoom; }

Use 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 Patterns

ComponentRequired approach
CarouselPrev/next/pause controls; don't require swiping; preserve scrolling and reduced-motion
MapNamed zoom/pan controls; address/coordinates/list alternative when the visual map alone isn't sufficient
SliderPrefer <input type="range">; provide numeric input or click-the-track alongside thumb dragging
Sortable listMove up/down/to controls in addition to drag-and-drop; announce result, preserve focus
Swipe actionExpose via a visible button/menu; swipe must not be the only way to delete/archive/reveal
Long pressProvide an ordinary button/menu; must not be the only route to a function
Drawing/signatureAvoid requiring fine path accuracy when not essential; offer typed/uploaded alternatives where the task allows
Canvas controlProvide 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.


Serious: Keep the Visible Label in the Accessible Name (WCAG 2.5.3)

Speech-input users (Dragon, iOS Voice Control) often activate a control by saying its visible label — the accessible name must contain that text.

<!-- 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>

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.


Preserve Orientation, Reflow, and Reachability

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.


Account for Touch Screen Readers

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.


Prevent Accidental Activation and Data Loss

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.


Testing

  • Review: inventory all controls/gestures; identify multipoint, path- based, dragging, swipe, long-press, pressure, tilt, and motion interactions; confirm each has the required single-pointer alternative (not just keyboard); measure actual target boxes in CSS pixels; search for restrictive viewport settings and broad touch-action rules; verify visible labels are in accessible names
  • Input: complete every task with touch alone, mouse/trackpad alone, pen (if supported), and keyboard alone; test speech input for visible labels; test touch exploration and activation with mobile screen readers; test a hybrid device switching between input modes. Use physical devices for final testing — emulation doesn't reproduce hand occlusion, reach, accidental contact, or sensor permissions reliably
  • Visual/layout: portrait/landscape; 200%/400% zoom; smallest supported viewport with on-screen keyboard; sticky/fixed content not obscuring controls; pinch zoom and page scroll from every custom gesture surface
  • Interaction state: press down, move away, release — confirm cancellation works where required; interrupt a direct-manipulation interaction with scrolling/orientation change/loss of pointer capture; confirm drag alternatives produce the same result and preserve focus; confirm motion input can be disabled with its alternative still available

Automated 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.


Common Failures

FailureCorrection
Browser zoom disabled because the layout breaksFix the responsive layout; remove viewport scaling restrictions
A keyboard command is the only alternative to a pinch/path gestureAdd controls that work with a single pointer without a path gesture
Keyboard reordering offered as the only drag alternativeAdd single-pointer Move controls that don't require dragging
Action fires on pointerdown for perceived responsivenessComplete 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 conformEvaluate the actual 24px circle test
Every link forced into a square targetLeave inline prose links in normal flow
Padding placed on a wrapper around a small icon buttonPut padding on the interactive element itself
Controls appear only on hoverProvide persistent or focus-triggered controls, touch-operable disclosure
pointer: coarse treated as a reliable touchscreen/stylus detectorTreat media features as capabilities; keep the base experience input-agnostic
Essential controls hidden unless pointer: fine matchesKeep core controls in the base layout
Separate touch and mouse handlers double-activateUse native click or a unified Pointer Events implementation
touch-action: none applied to a whole page/componentPreserve panning/zooming; restrict only the necessary gesture axis
Motion input is always active with no way to disableProvide a conventional alternative and a user-controlled disable
Mobile emulator is the only touch testTest physical devices, hybrid inputs, and mobile screen readers
Passing keyboard tests treated as proof of pointer accessibilityTest gestures, dragging, cancellation, and target size separately

Definition of Done Checklist

  • user-scalable=no and maximum-scale not in viewport meta tag
  • All multi-point/path-based gesture functionality has a single-pointer alternative in the content (not just a keyboard command)
  • All drag functionality has a single-pointer, non-dragging alternative
  • Interactive targets: minimum 24×24px (with valid exception if smaller); 44×44px recommended for primary controls
  • Padding for small targets is on the interactive element, not a wrapper
  • Actions fire on up-event (click, pointerup); destructive actions confirmable/undoable
  • Device motion functionality has a UI alternative and can be disabled
  • pointer/hover media queries only enhance an already-accessible base — never hide essential controls
  • Custom interactions use Pointer Events and handle pointercancel/lostpointercapture
  • touch-action scoped narrowly; page scroll and pinch zoom preserved elsewhere
  • aria-label values begin with or contain the visible label text (WCAG 2.5.3)
  • Sticky/fixed/overlay content doesn't obscure focused or operable controls
  • Tested on physical devices: iOS VoiceOver (touch), TalkBack (Android), mouse/trackpad, keyboard alone

Key WCAG Criteria

  • 1.3.4 Orientation (AA)
  • 1.4.4 Resize Text (AA) — Critical if zoom blocked
  • 1.4.10 Reflow (AA)
  • 1.4.13 Content on Hover or Focus (AA)
  • 2.1.1 Keyboard (A) — separately required alongside pointer alternatives
  • 2.5.1 Pointer Gestures (A) — Serious if multi-point gesture has no single-pointer alternative
  • 2.5.2 Pointer Cancellation (A) — Serious if actions fire on down-event
  • 2.5.3 Label in Name (A) — Serious for voice control users
  • 2.5.4 Motion Actuation (A)
  • 2.5.5 Target Size Enhanced (AAA) — 44×44px
  • 2.5.7 Dragging Movements (AA, WCAG 2.2) — Serious if drag has no single-pointer alternative
  • 2.5.8 Target Size Minimum (AA, WCAG 2.2) — Serious below 24×24px

References

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/

Signals

GitHub stars
44
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
touch-pointer
Source
github.com/mgifford/accessibility-skills