Gestures
A gesture has no label. Nobody sees a swipe or a double tap until they try it, so a gesture that is the only way to do something is a feature most people never find. Gestures also cost something you can feel: a double tap makes every single tap on the same view wait.
Tap each photo once.
Tap the photo
Tap the photo
Tap each photo once to select it. Double tap the left one to like it.
The left photo also listens for a double tap to like. After your first tap it can't know whether a second one is coming, so it waits out the double-tap window before it selects. The right photo has no double tap; the heart button likes it, and the tap answers in a frame.
Treat every gesture as a shortcut for something people can see and tap.
The vocabulary
People already know what these mean. Use them for the same things, and don't give a familiar gesture a meaning that only your app has.
| Gesture | What it does | Notes | |---|---|---| | Tap | Activate, select | Fires on lift | | Double tap | Zoom in and out | Delays single taps on the same view | | Touch and hold | More options: a context menu on iOS, selection on Android | See Long Press | | Swipe | Reveal row actions, dismiss, scroll, switch pages | See Swipe Actions | | Drag | Move an item, pull a sheet | Long press, then drag, reorders | | Pinch | Zoom, magnify | | | Rotate | Rotate a selected item | | | Edge swipe | Back | System owned, see below |
The double-tap tax
A single tap can't fire until the double tap has failed, and that takes a fixed window after the finger lifts.
- Android:
DOUBLE_TAP_TIMEOUTis 300 ms. Compose'sdetectTapGesturesholdsonTapback wheneveronDoubleTapis set, andcombinedClickable(onDoubleClick = …)does the same. - React Native Gesture Handler:
Exclusive(doubleTap, singleTap)waits up tomaxDelay, 200 ms by default. - iOS: a single tap that
require(toFail:)a double tap waits for it. Apple doesn't publish the window. - Flutter: a
GestureDetectorwith bothonTapandonDoubleTapholds the tap until the double tap loses;kDoubleTapTimeoutis 300 ms.
So add a double tap only where the single tap does nothing else, or where the wait doesn't matter, like double tap to zoom a photo that's already open. If like is the main thing people do in a feed, give it a button.
System gestures
Some swipes belong to the system. Don't build anything that competes with them.
- iOS: swipe in from the leading edge to go back. Since iOS 26, a pushed screen also goes back on a left-to-right pan anywhere in its content, so a horizontal carousel or chart scrubber there now competes with Back. Leave the bottom edge to the home indicator and the top edge to Notification Center and Control Center.
- Android: back is an inward swipe from either edge. You can exclude small strips, like a slider thumb, but the system honors at most 200dp of exclusion per edge. Apps can't opt out of the home and quick-switch swipes at the bottom.
See Back Navigation for building back itself.
Pan against scroll
A horizontal drag inside a vertical list has to decide which way it's going
before it takes the touch. Give it a small dead zone. I start the pan after
about 10pt of horizontal movement and let it fail if the finger moves 10pt
vertically first. That's activeOffsetX and failOffsetY in Gesture Handler.
Compose's draggable and anchoredDraggable wait for the 8dp touch slop in
their own direction, so a vertical LazyColumn still scrolls. In a pushed iOS
26 screen, a left-to-right pan also competes with the content back swipe;
in UIKit, make interactiveContentPopGestureRecognizer wait for yours with
require(toFail:).
Give every gesture a twin
Each gesture in this inbox has a visible equivalent. Use the gestures, then turn them off and do the same things with the ··· button.
- Maya Chen9:12Draft for Thursday
- Studio Ops8:40Your invoice is ready
- Leo ParkTuePhotos from the trip
- Nadia RahmanMonRe: the logo files
- CalendarSunDesign review moved
Swipe a row left or hold it. Then turn gestures off and do the same with ···.
With gestures off, nothing is out of reach. Apple's own rule for context menus is the same: "Always make context menu items available in the main interface, too." A gesture makes something faster for people who know it. The visible button is what makes it possible for everyone else, and for VoiceOver, TalkBack, Switch Control and keyboards.
Usage
// The gesture is a shortcut; the menu is always there.
MailRow(mail)
.swipeActions(edge: .trailing) {
Button("Archive", systemImage: "archivebox") { archive(mail) } // iOS 17+ initializer
}
.contextMenu { MailActions(mail) }
.overlay(alignment: .trailing) {
Menu { MailActions(mail) } label: {
Image(systemName: "ellipsis").frame(width: 44, height: 44)
}
.accessibilityLabel("Actions")
}// No onDoubleClick on a primary tap target: it delays every onClick.
Box(
Modifier.combinedClickable(
onClick = { open(photo) },
onLongClick = { select(photo) },
onLongClickLabel = "Select",
)
) {
PhotoImage(photo)
IconButton(onClick = { like(photo) }, Modifier.align(Alignment.BottomEnd)) {
Icon(Icons.Outlined.FavoriteBorder, contentDescription = "Like")
}
}// Gesture Handler 2.x (Expo SDK 57)
// Double tap to zoom an open photo, where the single-tap wait is fine.
const doubleTap = Gesture.Tap().numberOfTaps(2).onEnd(() => {
zoom.value = withSpring(zoom.value > 1 ? 1 : 2);
});
const singleTap = Gesture.Tap().onEnd(() => scheduleOnRN(toggleChrome));
const taps = Gesture.Exclusive(doubleTap, singleTap); // single waits up to 200 ms
// A horizontal pan inside a vertical list: decide the axis first.
const pan = Gesture.Pan().activeOffsetX([-10, 10]).failOffsetY([-10, 10]);// onDoubleTap makes onTap wait up to kDoubleTapTimeout (300 ms).
// Fine on an open photo, wrong on a feed card people tap all day.
GestureDetector(
onTap: toggleChrome,
onDoubleTap: toggleZoom,
child: PhotoView(photo),
)
// In the feed: tap opens, like is a visible button.
GestureDetector(
onTap: () => open(photo),
child: Stack(children: [
PhotoTile(photo),
Align(
alignment: Alignment.bottomRight,
child: IconButton(
icon: const Icon(Icons.favorite_border),
tooltip: 'Like',
onPressed: () => like(photo),
),
),
]),
)Custom gestures are invisible to assistive tech. VoiceOver and TalkBack replace touch with their own gestures, so a swipe or a double tap you built does nothing for them. Expose each one as an accessibility action, and keep the button on screen.
Resources
Apple HIG: Gestures - The standard iOS gestures, and why a custom gesture must never be the only way.
Ensure compatibility with gesture navigation - Android's back and home gestures, exclusion rects and the gesture insets.
interactiveContentPopGestureRecognizer - The iOS 26 back swipe that works from anywhere in a pushed screen.
Gesture Handler: Tap gesture - Tap defaults in React Native Gesture Handler, including the 200 ms gap between taps.
Using Swipe to Trigger Contextual Actions - NN/g on why hidden swipe actions are hard to find and need a visible route.