Press and hold the upper half of the pad and the page scrolls up; hold the lower half and it
goes down. How far from the centre you press sets the speed, so a touch near the middle creeps
and one near the edge travels — the same throttle law the hand sensor uses, driven by a
finger instead of a palm. A quick tap jumps one screen. Switch to Drag to throw
the page around with fling inertia instead, or take over the whole screen as one giant
pad. Works with touch, pen and mouse alike, because they are all just pointers.
01
One event model, three input devices
Pointer events unify mouse, pen and touch behind one interface, so nothing here
branches on device type. A mouse is simply a pointer that reports
pressure 0.5 and never has a second contact. Writing to touch events
alone would have excluded every desktop visitor for no benefit.
02
The browser wants this gesture
By default a drag on a touchscreen belongs to the browser's own scrolling, and it
takes it without asking — pointermove stops arriving partway through
and the element goes dead. touch-action: none is what declines that, and it
is the one line this page cannot work without.
03
Capture, or lose the finger at the edge
Slide off the pad mid-gesture and events stop unless the pointer was captured.
setPointerCapture redirects everything to the original element until
release, which is what lets a hold near the edge keep working when your finger drifts
outside.
04
Pressure is real, and mostly absent
Pens report genuine pressure, most touchscreens report a constant, and mice always
say 0.5 while a button is down. It is shown here because it is interesting,
not because anything depends on it — building a control that needs pressure
excludes almost every device.
05
A tap is a hold that changed its mind
There is no tap event worth having. A tap is defined here as a contact released
within 180 ms having moved less than 10 px — both conditions matter,
since a fast flick satisfies the first and should never be mistaken for a tap.
06
Fling is just velocity you kept
Drag mode samples the last few moves before release and keeps that velocity, then
decays it. Sampling the final event alone gives a number that is mostly noise; averaging
over roughly the last 100 ms is what makes a throw feel like it went where you
aimed it.
Hold above or below centre — distance sets the speed
How it works — the signal chain
Five stages between a finger and a scrollbar
Touch is the shortest of the three chains on this site — no model to load, no service
to call, no latency worth measuring. Which means every problem it has is a problem of
ownership: who gets the gesture, and who keeps it.
Stage 01
Claim
touch-action: none tells the browser not to interpret drags on this element
as scrolling. Declared in CSS rather than by calling preventDefault, so the
compositor knows before the gesture starts instead of after.
out → events keep arriving
Stage 02
Capture
setPointerCapture on pointerdown binds the pointer to the pad
until release, so a finger sliding past the edge keeps reporting instead of silently
vanishing.
out → pointer id
Stage 03
Normalise
Client coordinates become a 0–1 position within the pad, clamped. Everything
downstream works in that space, which is why the pad, the fullscreen veil and any future
surface can share one control law.
out → y · 0–1
Stage 04
Classify
Dead band, then direction, then a response curve — the same throttle used by the
hand sensor. On release, duration and travel decide whether it was a tap, a hold, or a
fling worth keeping velocity from.
out → velocity · px/s
Stage 05
Integrate
Velocity eases toward its target and accumulates into scrollTop, stopping
itself at either end. Identical to the other two sensors, because by this point the input
device has been abstracted away entirely.
out → scrollTop · px
The one line that matters
Why the gesture stops halfway through
Write this without touch-action: none and it will work perfectly with a mouse,
work in the desktop device emulator, and fail on a real phone in a way that looks like a
dropped event. You press, you drag, and a few frames in pointermove simply
stops. Nothing errors.
What happened is that the browser decided the gesture was a scroll. Touch gestures are
handled on the compositor thread so that scrolling stays smooth while JavaScript is busy,
which means the decision to claim a gesture is made before your handler runs and
cannot be undone from inside it. Calling preventDefault on the first
pointermove is already too late, and on a passive listener it does nothing at
all — touch listeners default to passive precisely so that scrolling cannot be blocked
by slow JavaScript.
touch-action is the declaration that resolves this. It is CSS, so the
compositor knows the element's intentions before a finger lands. none claims
every direction; pan-x would keep horizontal scrolling and claim only the
vertical, which is what a carousel wants.
Two gestures
Holding and throwing solve different problems
Gesture
How it behaves
Where it wins
ZonesDefault
Press and hold; position from centre sets a steady speed, held as long as you stay down.
Long documents and one-handed use. Your thumb never moves, nothing accelerates
away from you, and releasing always means stop. This is what a reading control should
do.
Drag
The page follows your finger directly, then keeps going when you throw it.
Short, precise moves — nudging a paragraph into view. Familiar because it is
what every native list does, but crossing a long document means repeated strokes.
The pattern across all three sensors on this site is the same: position for holding,
motion for throwing. The hand sensor's Hold and Wave modes are these two ideas driven by
a palm; voice reaches the same place with a latched direction, since a word cannot express a
distance at all.
Nothing is captured and nothing leaves the page. Unlike the hand and voice sensors,
this one needs no permission, no model and no network — pointer events are simply
delivered. The pad claims gestures only within its own bounds; the rest of the page scrolls
normally, and Esc stops motion and releases the fullscreen surface.