useDragGesture
The drag gesture behind Slider, RangeSlider, Joystick and Rating: spread its handlers, ref and style onto a View to get pointer samples in that View's own coordinates, which keep tracking after the finger leaves it, without the page or a ScrollView stealing the drag.
getGestureSurfaceStyle and GESTURE_RESPONDER_LOCK are the two pieces it's built on, for a PanResponder of your own.useDragGesture(options) returns { ref, onLayout, surfaceStyle, panHandlers, isDragging, getSurfaceRect }; put the first four on the same View (composing onLayout when you need your own) and give its children pointerEvents: 'none' so every press lands on the surface. onStart, onMove and onEnd each receive a DragPoint with x / y relative to the surface, its width / height (0 until the first layout) and dx / dy / distance travelled since the press, while onCancel fires instead of onEnd when the gesture is taken away. axis sets web touch-action: 'x' here leaves vertical page scrolling alone, 'both' (the default) claims every direction, and on web a drag also holds page-scroll and text-selection locks until release, cancel or unmount. claimOnStart: false with an activationDistance lets a tap fall through to something else, and enabled: false turns the surface back into ordinary content.Loading demo…
import { useState } from 'react';
import { Block, Text, useDragGesture } from '@plocks/ui';
import type { DragPoint } from '@plocks/ui';
const KNOB = 28;
// Children of the surface let the press through, so it always lands on the surface itself.
const PASS_THROUGH = { pointerEvents: 'none' } as const;
export function Demo() {
const [ratio, setRatio] = useState(0.5);
const [trackWidth, setTrackWidth] = useState(0);
const follow = (point: DragPoint) => {
const travel = point.width - KNOB;
if (travel <= 0) return;
setRatio(Math.min(1, Math.max(0, (point.x - KNOB / 2) / travel)));
};
const drag = useDragGesture({
axis: 'x',
cursor: 'grab',
activeCursor: 'grabbing',
onStart: follow,
onMove: follow,
});
return (
<Block fullWidth maw={360}>
<Block
ref={drag.ref}
onLayout={(event) => {
drag.onLayout(event);
setTrackWidth(event.nativeEvent.layout.width);
}}
style={drag.surfaceStyle}
{...drag.panHandlers}
h={KNOB}
justify="center"
>
<Block h={4} radius="full" bg="border" style={PASS_THROUGH} />
<Block
position="absolute"
top={0}
left={ratio * Math.max(0, trackWidth - KNOB)}
w={KNOB}
h={KNOB}
radius="full"
bg="primary.5"
opacity={drag.isDragging ? 0.8 : 1}
style={PASS_THROUGH}
/>
</Block>
<Text ta="center" ff="monospace">
{Math.round(ratio * 100)}%
</Text>
</Block>
);
}React Native design for iOS, Android, and Web.
Quick Links
Resources