Chapter 25
The gesture
Touching isn't clicking: the finger follows, drags, pulls. But every gesture here has a button and a shortcut that do the same thing — because a gesture is an accelerator, not a gate.
The mouse points and clicks: a target, a hit. The finger does something different — it follows. It drags, pulls, swipes, keeps contact while the thing moves. When an interface stops being clicked and starts being touched, the grammar changes: no longer a point and an action, but a continuous trajectory that the finger leads and the interface obliges. The gesture is a dialogue, not a command.
But here’s the trap half of “mobile-first” design falls into: mistaking the gesture for the function. Swipe-to-dismiss is elegant — until it’s the only way to dismiss. Then it becomes a gate: whoever doesn’t know it exists, whoever can’t drag precisely, whoever navigates by keyboard or screen reader is left out. An invisible, mandatory gesture isn’t a shortcut: it’s a door with the handle hidden. The rule of this chapter is one, and we repeat it in each of the five demos: a gesture is an accelerator, not a gate. Every swipe has its button, every pull has its “Refresh”, every shortcut has its explicit path.
And there’s a second commitment, more technical but just as much about respect: the gesture stays home. No global handlers hijacking the whole page’s scroll because one box wanted pull-to-refresh; no ? key opening an overlay while you’re reading somewhere else. Pointer capture, overscroll-behavior: contain, listeners bound to the component and not to window: containment is the difference between a gesture that helps and one that takes over everything. Five gestures, five explicit equivalents, zero hijacked page.
Dismiss with a finger, or with a button
The first gesture is swipe-to-dismiss: a card you drag away. The finger moves it 1:1, with resistance growing near the threshold, and it either flies off or springs back. But beside it there’s always the “Dismiss” button.
Notification
Three people commented on your chapter. Swipe it away to archive it.
Drag sideways to dismiss — or use the button.
Swipe-to-dismiss · the finger follows, but there’s a button
Drag the card sideways: it follows the finger exactly, with resistance building past the midpoint — the square root of the displacement saying “I’m giving, but you’ll have to insist”. Past the threshold it leaves from the side of the gesture; below it, it returns into place with a soft curve. We use Pointer Events with setPointerCapture and touch-action: pan-y, so the horizontal drag never steals the page’s vertical scroll: the gesture stays inside the card.
Here’s the chapter’s honesty. The swipe is an accelerator for those who know it — the “Dismiss” button does the exact same thing, reachable with Tab and Enter, and after dismissal focus moves to “Restore” so nobody is orphaned. If the swipe were the only way, it would be a gate: keyboard users couldn’t archive anything. With reduced motion the elastic return and the decorative exit collapse via tokens: the card disappears all the same, only the journey is gone.
Pull to refresh, without abducting the page
Pull-to-refresh is the quintessential mobile gesture, and also the easiest to get wrong: one careless handler and the whole page bounces. Here the gesture lives inside a scrollable container, and stays there.
- Draft saved
- New comment
- Chapter published
- Mention received
- Backup completed
Pull from the top of the list — or press Refresh. The gesture stays inside the frame.
Pull-to-refresh · contained in its frame
When you’re already at the top of the list and pull down further, the indicator descends following the finger with resistance; past the threshold, on release the refresh fires. Containment is everything: overscroll-behavior: contain stops the box’s scroll from overflowing onto the page, and the JS gesture arms only when scrollTop is 0 — the moment there’s content above, the pull goes back to being normal scroll. No listener on window, no hijacking of the global scroll.
The accelerator always has its explicit twin: the “Refresh” button up top does the same thing, from the keyboard, without needing to know the gesture exists. And the state stays honest — no inflated counts on return, just “updated just now”, and the spinner turns only while the work is genuinely in progress, past the threshold of decency. With reduced motion the spinner doesn’t rotate: the indicator appears, the state changes, but without the decorative spin.
The arrows for fingers already trained
Not every gesture is the finger’s. Vim-style navigation — j/k to go up and down, h/l to close and open — is a keyboard gesture: invisible, optional, lightning-fast for those who know it. The risk is that it becomes a secret language.
Keyboard navigation active
j / k to move, h / l to close or open — arrows and Tab work anyway.
Vim navigation · optional shortcut, never forced
Focus the list and j/k move you between items, h/l close and reopen the detail. But it’s an extra: the ↑/↓ arrows, Home/End, Tab and click work exactly as always. The list is a roving-tabindex menu — one tabbable item at a time — so the standard keyboard comes for free and j/k merely accelerate it. Focus is always visible: the —halo-focus ring plus a bar beside the item, because position isn’t read from colour alone.
Two honesty details. First: the keys are scoped — the keydown is bound to the list, not to window, so “j” isn’t intercepted while you read elsewhere on the page. Second: a hint appears on focus (“Keyboard navigation active”), because a shortcut nobody announces isn’t a shortcut, it’s a secret. The accelerator must declare itself, or it helps no one but those who already knew.
The legend that appears only where you are
The ? key that opens the list of shortcuts is a classic. Almost always it’s global — listening across the whole page. Here we make it local: the legend opens only when this panel has focus.
Focus below and press ?
ShortcutHint · the “?” scoped to the component
Focus the panel and press ?: a <dialog> opens with the local shortcuts, closed by Esc. Unlike the global overlay of chapter 04, here the keydown is bound to the component, not to document: the legend won’t intrude while you’re elsewhere, and several “islands” of shortcuts can coexist without fighting over the same key. Using showModal(), focus trap, top-layer and focus restoration arrive natively — no hand-written trap.
The accelerator stays one: there’s always the visible “Shortcuts” button that opens the same panel, for whoever knows nothing of the key. And ? doesn’t fire while you’re typing in a field — someone typing “why?” wants the question mark, not an overlay in their face. It’s the same courtesy as ever: the quick gesture must never steal the obvious one.
When a panel is open, the rest is out of play
The last one isn’t a finger gesture but the structural gesture every overlay must make: declaring what’s interactive. When a panel opens, the backdrop should become inert — not focusable, not clickable, invisible to screen readers.
Background content
Try pressing Tab or clicking these buttons while the panel is open.
Foreground panel
While I am open, the backdrop should be out of play. With inert on, Tab can’t reach it and clicks don’t pass through. Turn inert off and feel the difference.
InertBackdrop · with and without inert, compared
Open the panel and try pressing Tab or clicking the backdrop buttons. With inert on, the backdrop is truly out of play: Tab can’t reach it, clicks don’t pass, assistive tech skips it — one attribute removes focus, pointer events and ARIA visibility together. Uncheck “Use inert” and try again: Tab falls behind the panel and the “covered” buttons still respond — the proof, written on screen, of why it’s needed.
The <dialog>.showModal() makes the backdrop inert on its own; but when the panel is in-page, as here, it’s on us. It’s the other face of the chapter: an interface that’s touched must know, at every instant, what can be touched. The inert backdrop is also visibly dimmed — blurred and muted — because the “non-interactive” state doesn’t rely on behaviour alone, it’s seen. inert is correctness, not decoration: it stays identical even with reduced motion.
Five gestures, one discipline: touch adds speed, never requirements. The swipe that dismisses has its button, the pull that refreshes has its own, Vim navigation has its arrows, the ? has its icon, and every panel declares with inert what stays alive beneath it. A gesture is honest when it’s a bonus for those who know it and nothing lost for those who don’t — and when it stays home, without seizing the scroll, the focus or the keyboard of the rest of the page. The interface that’s touched is more alive than the one that’s clicked, but only if no one, touching it or not, is locked out.
The next chapter closes the circle, and shifts tone. After twenty-five chapters spent bringing the user in — holding them, serving them, not disturbing them — the last question is the most neglected: how do you let them go? The farewell — how to take leave of a user with grace, when they’re done, without grabbing them by the arm.