# Pinch, drag and tilt gestures with no button

Source: https://easeweb.dev/learn/gestures
Topics: Page structure > Gestures, dragging and shaking have a simple alternative (WCAG 2.5.1, 2.5.4, 2.5.7)
The fix: Keep the drag, the pinch and the tilt as shortcuts, and put the same actions on buttons beside the view: Turn left, Turn right, Zoom out and Zoom in, with the zoomed photo in a box the browser scrolls. Leave turning by tilt off until a switch on the page turns it on, and let the same switch turn it off. Arrow keys on a focused view are not enough on their own: they help keyboard users, but someone with a mouse or one finger still has to drag and pinch.
Test with: mouse, touch, device motion, keyboard, speech input, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Understanding/motion-actuation.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F105 https://www.w3.org/WAI/WCAG22/Techniques/failures/F106 https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/requestPermission_static

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A homeware shop shows an enamel mug in a 360-degree view: twelve photos taken round it, one every 30 degrees. Under the photo it says "Drag to turn. Pinch to zoom." Drag sideways and the mug turns. Spread two fingers on a phone and it zooms in, and once zoomed, a drag moves the photo around so you can read the small print on the back. On a phone, the mug also turns as the phone tilts, from the moment the page loads. There are no buttons. With a mouse, the zoom can't be reached at all, and the drag is the only way to see the back of the mug. Lay the phone in a stand or hold it in a shaking hand, and the mug never stops turning.

## Why it fails

Each action hangs on a gesture, and none has a control of its own. The pinch is a multipoint gesture, and WCAG 2.5.1 asks for every function that uses one to work with a single pointer and no path, such as a tap on a Zoom in button; failure F105 names the missing alternative. Turning the mug and moving the zoomed photo need the pointer held down while it moves, which is a dragging movement, and 2.5.7 asks for a way to do the same with single clicks or taps (F108). The tilt is device motion, and 2.5.4 asks for two things: a control that does the same, and a way to turn the response to motion off (F106). No browser tells a page about the phone's own setting for motion input, so that switch has to be on the page.

## Who is affected

On a phone, the view often fills the width of the screen. With `touch-action: none`, a finger that lands on the photo can't scroll the page either, so the gestures also trap anyone who only wanted to get past the mug to the price.

## What automation and AI miss

The markup is an image in a figure, with alt text and a caption, and every scanner passes it. A pinch handler, a drag handler and a `deviceorientation` listener leave nothing in the DOM. The audit asks the browser for the page's listeners and flags a page that listens for device motion or orientation, and a large picture or canvas that takes every touch for itself with `touch-action: none` and has no button in it or beside it. Both are leads for a person: only someone trying the view can tell whether each gesture has another way in. Asked to fix it, AI tools often make the view focusable and turn it with the arrow keys, which helps keyboard users but still leaves a mouse or one finger to drag and pinch; or add `aria-label="Drag to rotate"`, which tells a screen reader user about a gesture they can't make; or stop the tilt only under `prefers-reduced-motion`, which is about animation on screen, helps only people who found that setting, and still leaves no button.

## Before: a view that only gestures work

```html
<figure class="spin-view">
  <div class="spin-frame">
    <img src="mug-00.jpg" alt="Enamel mug">
  </div>
  <figcaption>Drag to turn. Pinch to zoom.</figcaption>
</figure>
```

```css
.spin-frame {
  overflow: hidden;
  touch-action: none;
}
```

```js
const FRAMES = 12; // one photo every 30 degrees
// Drag sideways to turn; once zoomed in, drag to move the photo
frame.addEventListener('pointermove', (event) => {
  if (!dragging) return;
  if (zoom > 1) panBy(event.movementX, event.movementY);
  else turnTo(startFrame + Math.round((startX - event.clientX) / 20));
});
// Two fingers apart or together set the zoom
function pinchToZoom(event) {
  if (event.touches.length !== 2) return;
  zoomTo((startZoom * spread(event.touches)) / startSpread);
}
frame.addEventListener('touchmove', pinchToZoom);
// Tilting the phone turns the mug, from the moment the page loads
window.addEventListener('deviceorientation', (event) => {
  turnTo(Math.round(event.gamma / 10));
});
```

Turning and moving the photo need a drag, zooming needs two fingers, and the tilt can't be turned off. Nothing on the page is a control, so a single click, a tap, a key or a spoken command has nothing to act on.

## After: buttons beside the view, and tilting as a switch

```html
<figure class="spin-view">
  <div class="spin-frame" tabindex="0" role="region" aria-label="Zoomed photo">
    <img src="mug-00.jpg" alt="Enamel mug">
  </div>
  <figcaption>Drag to turn. Pinch to zoom.</figcaption>
  <div class="spin-controls">
    <button type="button" data-turn="-1">Turn left</button>
    <button type="button" data-turn="1">Turn right</button>
    <button type="button" data-zoom="-1">Zoom out</button>
    <button type="button" data-zoom="1">Zoom in</button>
    <button type="button" class="tilt-switch" aria-pressed="false" hidden>Turn by tilting the phone</button>
  </div>
</figure>
```

```css
/* Zoomed in, the browser scrolls the photo: scroll bars, the wheel, the arrow keys and one finger */
.spin-frame {
  overflow: auto;
  touch-action: pan-y;
}
.spin-frame.zoomed {
  touch-action: pan-x pan-y;
}
.spin-frame img {
  width: calc(var(--zoom, 1) * 100%);
  max-width: none;
}
```

```js
// The drag and the pinch stay as they were, as shortcuts. The drag now moves the same scroll.
const panBy = (dx, dy) => frame.scrollBy(-dx, -dy);

controls.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-turn], button[data-zoom]');
  if (!button) return;
  if (button.dataset.turn) turnTo(current + Number(button.dataset.turn));
  else zoomTo(zoom + Number(button.dataset.zoom));
});

// Zoom from 1 to 3 times, keeping the middle of the view where it was
function zoomTo(level) {
  const x = (frame.scrollLeft + frame.clientWidth / 2) / frame.scrollWidth;
  const y = (frame.scrollTop + frame.clientHeight / 2) / frame.scrollHeight;
  zoom = Math.min(3, Math.max(1, level));
  frame.style.setProperty('--zoom', zoom);
  frame.classList.toggle('zoomed', zoom > 1);
  frame.scrollLeft = x * frame.scrollWidth - frame.clientWidth / 2;
  frame.scrollTop = y * frame.scrollHeight - frame.clientHeight / 2;
}

// Tilting turns the mug only after the switch is pressed, and stops when it is pressed again
const tilt = document.querySelector('.tilt-switch');
tilt.hidden = !('DeviceOrientationEvent' in window && matchMedia('(pointer: coarse)').matches);
let heldAt, from;
function turnWithTilt(event) {
  heldAt ??= event.gamma; // the angle the phone was held at when the switch went on
  turnTo(from + Math.round((event.gamma - heldAt) / 10));
}
tilt.addEventListener('click', async () => {
  const on = tilt.getAttribute('aria-pressed') !== 'true';
  // iOS Safari asks the person first, and only from a tap
  if (on && typeof DeviceOrientationEvent.requestPermission === 'function'
      && (await DeviceOrientationEvent.requestPermission()) !== 'granted') return;
  tilt.setAttribute('aria-pressed', String(on));
  if (on) {
    heldAt = null;
    from = current;
    window.addEventListener('deviceorientation', turnWithTilt);
  } else {
    window.removeEventListener('deviceorientation', turnWithTilt);
  }
});
```

Each press of Turn left or Turn right shows the next photo round, so twelve presses go all the way round, and Zoom in and Zoom out step from one to three times. A click, a tap, Enter, Space and "Click Turn right" all work, because they are buttons. Moving the zoomed photo is left to the browser: the frame scrolls, so a mouse uses its scroll bars or wheel, a keyboard the arrow keys once the frame has focus, and a finger flicks it as it would any scrolling box. The tilt does nothing until someone presses the switch, which reports "on" or "off" through `aria-pressed`, and pressing it again removes the listener. On iOS, the same press is the tap that Safari needs before it will ask for permission to read the motion sensors. `touch-action: pan-y` lets a finger on the photo scroll the page past it, and the site still gets two-finger touches for its own pinch.

## Implementation decisions

Keep the gestures. Dragging and pinching are quick for the people who can do them, and the criteria ask for another way, not a replacement. The buttons go in the page, under or beside the view, not on a toolbar that appears only after a tap on the photo: a control that a gesture has to uncover is no alternative to it.

Motion off by default is the safer choice. Leaving the tilt on with a visible switch to stop it also meets 2.5.4, but the mug turns for everyone before they find the switch, including the people who can't hold the phone still, and on iOS the page has to ask for permission from a tap anyway. Show the switch only where motion sensors are likely, as the snippet does, so desktop visitors don't get a control that does nothing.

The same reasoning covers the other places where a gesture or motion runs something on a page:

- Shake to undo, shake to shuffle or shake for a discount: give the same action a button, and leave shaking off until it is switched on.
- Double-tap to zoom and click to zoom on the photo: single-pointer actions that pass 2.5.1 and 2.5.7 on their own, but nothing shows them and a keyboard or voice can't reach them. Keep them as extra shortcuts beside the buttons.
- Pinch-zoom and pan on a map: the same plus and minus buttons, and arrow buttons or a scrolling map for moving it, as the W3C's example for 2.5.1 describes.
- Viewer libraries: many 360-degree and zoom viewers have methods for the next frame and for zoom even when they draw no buttons. Wire real `<button>` elements to them, and check any buttons the library does draw are buttons, not `div`s.

A turn button should move by a step that matches what the drag reaches. With twelve photos, one per press is fine. With 72, a press of one photo makes the trip round long; step by three or six, as long as every side the drag can show is still shown. Icon buttons need names people would say, such as "Turn left", as [icon-only buttons with no accessible name](/learn/icon-buttons) explains. Don't block the browser's own zoom while fixing the viewer's: a viewport tag with `user-scalable=no` fails 1.4.4, as [enlarged text cut off by fixed-height boxes](/learn/resize-text) notes. A screen reader user also needs to know what the views show, which is a matter of alt text and a description, not of these criteria.

## Verify the fix

1. With a mouse, using single clicks only, turn the product all the way round, zoom in, and bring the small print into view. No step may need the button held down while the pointer moves.
2. On a phone, do the same with single taps of one finger.
3. Load the page on a phone and tilt and shake it: nothing turns. Turn motion on with the switch, check that tilting turns the product, then turn it off and check that it stops.
4. From the keyboard, Tab to each button and press it. With voice control, say "Click Turn right" and "Click Zoom in". With a screen reader, check each button is read with its name, and the motion switch with its state.
5. On a phone, at the starting zoom, start a scroll with a finger on the photo: the page scrolls past it.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the viewer or its library changes.

## Limits

This is a starting pattern, not a guarantee for every viewer. Both 2.5.1 and 2.5.4 make an exception where the gesture or the motion is essential, such as a signature drawn with a finger, a step counter or a game played by tilting; check that claim honestly before relying on it. Other gestures have guides of their own: swiping between slides in [carousels that keyboard and screen reader users can't use](/learn/carousels), dragging a thumb in [range sliders that only move by dragging](/learn/sliders), dragging cards in [drag-and-drop boards that only a mouse can use](/learn/drag-and-drop), and swiping or long-pressing a row in [right-click, long-press and swipe actions with no button](/learn/context-menus). Test the complete journey on the phones and assistive technology your customers use.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
