SuperAPI AI Directory

touch-input

General & other

View repository

Updated 10-15 19:52

About this skill

Designing for the Thumb

Assume the pointer is coarse, imprecise, attached to a hand that covers part of the screen, and incapable of hovering. That single assumption decides most of what follows: hit areas grow past their visuals, hover reveals but never enables, feedback arrives on press rather than on release, and every gesture has to declare which axis it wants before the browser has to guess. Design the touch version as the real one and let the fine-pointer version be the variant — the reverse produces interfaces that are technically usable on a phone and miserable in practice. gestures owns everything that happens once the finger starts moving — velocity, springs, rubber-banding, mid-flight interruption; this skill stops at the moment of contact: size, placement, latency, and which axis wins. Keyboard order, focus and ARIA are a11y; whether the app reads as installed is native-feel.

Work in the project's existing responsive and styling system. If it has a hover: variant strategy, a size-* scale, or a touch-target utility, extend that rather than adding raw media queries beside it. Check whether an interaction library (Vaul, Embla, use-gesture, Radix) already owns touch-action and pointer capture on the component you are touching — two things setting touch-action on the same element is a bug that only reproduces on device. If the project targets desktop only today, add capability queries anyway; they cost nothing and a touchscreen laptop is already in the wild.

Topic Reference
A bug that only reproduces on an iPhone Open references/platform-quirks.md when something works everywhere except iOS Safari — input zoom, video autoplay, viewport height, tap highlight, scroll locking.

Core Principles

  1. The hit area is 44×44; the visual can be anything. Apple's HIG sets 44×44pt as the minimum comfortable target, and a 24px icon button with a 24px hit area is the single most common touch defect. Keep the icon at 24px and expand the target with min-width: 44px; min-height: 44px, or a pseudo-element at inset: -10px. Exception: a target inside a run of inline text cannot be 44px tall without wrecking the line — WCAG 2.2 SC 2.5.8 puts the hard floor at 24×24 CSS px and demands clear spacing whenever you sit under 44.

  2. Adjacent targets need dead space between them. The finger's contact patch is far wider than the single point the browser reports, so two hit areas that touch will fire the wrong one at the edges. Leave at least 8px of untargeted space between independent controls (house default), and check that expanded hit areas do not overlap — a delete whose invisible padding reaches under a confirm is a data-loss bug that no screenshot shows. Exception: segmented controls, paginators and calendar grids, where the options are adjacent by design and none is destructive.

  3. Gate hover on capability, never on width. Touch devices fire :hover on tap and leave the state stuck until the next tap elsewhere. Wrap every hover style in @media (hover: hover) and (pointer: fine) (Emil Kowalski). A viewport-width breakpoint is the wrong test: a touchscreen laptop is wide and coarse, and a phone in a desktop-mode browser is neither. Exception: a purely decorative cursor-follow effect may gate on (pointer: fine) alone, since it has no state to get stuck in.

  1. Nothing may be reachable only by hover. Hover reveals and embellishes; it must never be the sole route to an action. A row's delete button that appears on hover is invisible on touch, and "long-press to discover" is not a discoverable affordance. Give coarse pointers a persistent control — usually a visible overflow button. Exception: a hover shortcut that duplicates an action already present in an open menu or toolbar.

  2. Respond on pointerdown, commit on pointerup. Waiting for click to show the pressed state reads as lag, because the finger is already lifting by then. Set the press state on pointerdown, run the action on pointerup inside the target, and cancel if the finger leaves — with roughly 10px of slop so a shaky release still commits. Exception: destructive actions commit on release only, never on press, and must stay cancellable by dragging away.

  3. Remove residual tap latency with touch-action, not with a library. The historical 300ms double-tap-zoom delay is gone on any page with <meta name="viewport" content="width=device-width">, but an element that is still double-tap-zoomable pays it. Set touch-action: manipulation on buttons, links and inputs; touch-action: none on a surface that implements its own pan or zoom. Exception: never put touch-action: none on a scroll container — you delete scrolling, and it only shows up on device.

  4. Declare the scroll axis before the gesture starts. A horizontally swiping row inside a vertical scroller forces the browser to wait for JS to decide who owns the gesture, and the first frames are lost. Declare it in CSS: touch-action: pan-y on children of a vertical scroller that also drag sideways, pan-x on a horizontal carousel inside a scrolling page. Claim the axis on the first move and hold it for the life of the gesture — an arbitration that flips mid-drag feels broken. Exception: a full-screen canvas that genuinely owns both axes takes none. Velocity thresholds and what happens after the axis is claimed are gestures.

  5. Contain scroll at panel boundaries. Scrolling to the end of a modal, drawer or dropdown must not start scrolling the page behind it — on iOS the page keeps its scroll position and the user loses their place. Put overscroll-behavior: contain on every independently scrolling panel. Exception: overscroll-behavior-y: none on the document root also kills pull-to-refresh — correct for an app-like surface, hostile on a content page.

  6. Put the primary action where the thumb already is. On a large phone held one-handed, the comfortable arc is the bottom third on the holding side; the diagonally opposite top corner is the worst reach on the device. Anchor primary actions to the bottom or make them full-width, and deliberately place destructive actions outside the natural arc so the reach itself acts as a confirmation. Exception: on desktop and tablet a bottom-anchored primary action reads as an unported mobile layout — anchor it to the form instead. Home-indicator and notch insets are responsive.

Smells and Fixes

Smell Fix
.icon-btn { width: 24px; height: 24px } Keep the icon 24px, give the button min-width/min-height: 44px or ::before { inset: -10px }
Hover styles with no media query Wrap in @media (hover: hover) and (pointer: fine)
Hover gated on @media (min-width: 1024px) Gate on capability; width does not imply a fine pointer
Row action visible only on hover Add a persistent overflow control for coarse pointers
Pressed state applied in a click handler Apply on pointerdown, commit on pointerup
touch-action: none on a scrollable list pan-y if children drag sideways, otherwise remove it
Drawer scrolls the page behind at its end overscroll-behavior: contain on the drawer
A fastclick-style tap library in the deps Delete it; touch-action: manipulation plus a correct viewport meta replaces it
Two libraries setting touch-action on one node Pick one owner; the second silently wins in ways that differ per browser

Checklist

  • Every target is at least 44×44, and never under the 24×24 floor
  • At least 8px of dead space between adjacent independent targets; expanded hit areas do not overlap
  • Every hover style is inside @media (hover: hover) and (pointer: fine)
  • No action is reachable only by hover or only by long-press
  • Press feedback fires on pointerdown; destructive actions commit only on release
  • touch-action: manipulation on buttons, links and inputs
  • Every drag inside a scroller declares its axis with pan-x / pan-y
  • overscroll-behavior: contain on modals, drawers and scrolling panels
  • Primary actions sit in the bottom arc on phones
  • Tested on a real device, not just a resized desktop window