Anything done with a swipe or pinch must also work with a single tap or click. What this means for city and county apps.
A multipoint gesture uses two or more fingers at once, such as a two-finger pinch. A path-based gesture depends on the route the pointer takes, such as a swipe that has to travel in a mostly straight line or a loop drawn on a map. WCAG 2.1 success criterion 2.5.1 says that anything you can do with one of those gestures must also be doable with a single pointer (one finger, a mouse, a stylus) without a path-based gesture, for example with a tap, a click or a button.
Keyboard support does not satisfy this criterion on its own. Some people cannot use a keyboard but can still tap or click, so the single-pointer alternative should be a control that works by pointer, ideally a visible one. The criterion allows a gesture to stay when it is essential, such as a signature, and it does not cover gestures reserved by the browser, the operating system or assistive technology.
Government and municipal sites often reuse consumer widgets: a park reservation calendar that changes month only on swipe, a library app where a loan is renewed by sliding the row sideways, a permit viewer for scanned plan sets that zooms only by pinching, or a GIS parcel search that asks residents to draw a loop around their street. Each of these is a service a resident has to complete. Since the Title II rule adopts WCAG 2.1 Level A and AA, a gesture-only control in a city or county app is judged against this criterion. Mobile apps are covered too; see mobile apps.
let startX = 0;
calendar.addEventListener("touchstart", e => { startX = e.touches[0].clientX; });
calendar.addEventListener("touchend", e => {
const dx = e.changedTouches[0].clientX - startX;
if (dx < -60) showMonth(current + 1); // swipe is the only way
if (dx > 60) showMonth(current - 1);
});<button type="button" id="prev-month">Previous month</button>
<button type="button" id="next-month">Next month</button>
<script>
document.getElementById("prev-month")
.addEventListener("click", () => showMonth(current - 1));
document.getElementById("next-month")
.addEventListener("click", () => showMonth(current + 1));
// The swipe handler may remain as a shortcut.
</script><li class="loan" data-reveal-on-swipe="renew">
<span>The Grapes of Wrath</span>
<span>Due May 3</span>
</li><li class="loan">
<span>The Grapes of Wrath</span>
<span>Due May 3</span>
<button type="button" data-renew="1042">Renew</button>
</li>viewer.addEventListener("touchmove", e => {
if (e.touches.length === 2) {
zoomBy(distance(e.touches) / lastDistance); // two fingers only
}
});<button type="button" id="zoom-in">Zoom in</button>
<button type="button" id="zoom-out">Zoom out</button>
<script>
document.getElementById("zoom-in")
.addEventListener("click", () => zoomBy(1.25));
document.getElementById("zoom-out")
.addEventListener("click", () => zoomBy(0.8));
// Two-finger pinch can still call zoomBy().
</script>Native apps hit the same problem with custom gesture recognizers. Keep the gesture if you like it, but add a button or menu item that does the same job with one finger.
// SwiftUI
Image("floorplan")
.scaleEffect(scale)
.gesture(MagnificationGesture().onChanged { scale = $0 })// SwiftUI
VStack {
Image("floorplan").scaleEffect(scale)
.gesture(MagnificationGesture().onChanged { scale = $0 })
HStack {
Button("Zoom out") { scale = max(1, scale - 0.25) }
Button("Zoom in") { scale = min(4, scale + 0.25) }
}
}// Jetpack Compose
Image(
painter = painterResource(R.drawable.floorplan),
contentDescription = "Clinic floor plan",
modifier = Modifier.pointerInput(Unit) {
detectTransformGestures { _, _, zoom, _ -> scale *= zoom }
}
)// Jetpack Compose
Column {
Image(
painter = painterResource(R.drawable.floorplan),
contentDescription = "Clinic floor plan",
modifier = Modifier
.graphicsLayer(scaleX = scale, scaleY = scale)
.pointerInput(Unit) {
detectTransformGestures { _, _, zoom, _ -> scale *= zoom }
}
)
Row {
Button(onClick = { scale = (scale - 0.25f).coerceAtLeast(1f) }) { Text("Zoom out") }
Button(onClick = { scale = (scale + 0.25f).coerceAtMost(4f) }) { Text("Zoom in") }
}
}touchstart, touchmove and pointermove handlers helps you build the list, but a tool cannot tell whether a button alternative exists. That takes a person.