useDragValue
Drive a number with a pointer drag: the range, the step and the scale are the hook's, so the handler receives a value rather than pixels.
SourceThis is what Knob, Slider and XYPad are built on. Reach for it when a
control of your own needs the same drag behaviour.
function App() { const areaRef = useRef<HTMLDivElement>(null) const [value, setValue] = useState<[number, number]>([50, 50]) const { refCallback, dragging } = useDragValue<HTMLDivElement>({ // y is reversed so that its maximum is at the top. axis: [ { min: 0, max: 100, step: 1 }, { min: 0, max: 100, step: 1, reverse: true }, ], baseElementRef: areaRef, updateOnPointerDown: true, onChange: (value) => setValue(value), }) // The element that starts the drag is also the one the pointer is measured // against, so it takes both refs. const ref = useComposedRefs<HTMLDivElement>(areaRef, refCallback) return ( <div style={{ userSelect: 'none' }}> <div ref={ref} style={{ position: 'relative', width: '100%', height: 160, marginBottom: '0.5rem', border: '1px solid currentColor', borderRadius: 8, touchAction: 'none', }} > <div style={{ position: 'absolute', left: `${value[0]}%`, top: `${100 - value[1]}%`, width: 16, height: 16, borderRadius: '50%', background: 'currentColor', translate: '-50% -50%', pointerEvents: 'none', }} /> </div> <div>dragging: {String(dragging)}</div> <div> x: {value[0]}, y: {value[1]} </div> </div> ) }
Import
import { useDragValue } from '@tremolo-ui/react'
The value is yours to hold
The hook keeps no value. onChange is where it is stored, and the current
value is read back through getValue — or, with baseElementRef, not read at
all because the pointer position says what it is. That is what lets the same
value be set from a text field, a MIDI message or a preset without the drag
and the state drifting apart.
Two ways to map a drag
Which one applies is decided by the option you pass. Give exactly one of them.
baseElementRef: the value is the position pointed at
The pointer is normalized against the bounding rect of the element, so the
value follows the pointer exactly. updateOnPointerDown makes a press jump the
value there before the pointer moves at all. This is how Slider and XYPad
behave.
getValue: the value moves by the distance dragged
Where the pointer is carries no meaning — the value moves away from where it
stood, with pixelRange pixels covering the whole range. This is how a knob
follows a drag, and why the value does not jump to the pointer.
function App() { const [value, setValue] = useState(50) const { refCallback, dragging } = useDragValue<HTMLDivElement>({ axis: { min: 0, max: 100, reverse: true }, // The value moves away from where it stood, so the hook has to be able to // read it. 200px of movement covers the whole range. getValue: () => [0, value], pixelRange: 200, // Hold shift to cover a tenth of the range with the same movement. sensitivity: (state) => (state.event.shiftKey ? 0.1 : 1), cursor: 'ns-resize', onChange: ([, y]) => setValue(y), }) return ( <div style={{ userSelect: 'none' }}> <div ref={refCallback} style={{ padding: '2rem', marginBottom: '0.5rem', border: '1px solid currentColor', borderRadius: 8, textAlign: 'center', touchAction: 'none', }} > Drag up and down </div> <div>dragging: {String(dragging)}</div> <div>value: {value.toFixed(1)}</div> </div> ) }
Fine adjustment
sensitivity is read on every move, so it can answer to a modifier key held
part way through a drag:
useDragValue({
sensitivity: (state) => (state.event.shiftKey ? 0.1 : 1),
})
With baseElementRef the value is normally the position pointed at, so
anything but 1 turns the mapping relative and leaves the pointer and the
value apart for the rest of the drag.
Axes
axis takes one AxisOptions for both axes, or [x, y] for one each. Besides
min, max, step and scale it takes reverse, which is how the y axis is
made to grow upwards — the screen's y grows downwards.
A control that moves on one axis only still gets a value for both; ignore the one you do not use, as the relative example above does.
Options
| Prop | Type | Default |
|---|---|---|
axis* | AxisOptions | readonly [AxisOptions, AxisOptions] | — |
baseElementRef | RefObject<Element | null> | — |
getValue | () => XY<number> | — |
pixelRange | number | readonly [number, number] | 100 |
sensitivity | (state: DragState) => number | — |
updateOnPointerDown | boolean | — |
threshold | number | 0 |
cursor | string | — |
pointerLock | boolean | false |
shouldStart | (event: PointerEvent) => boolean | — |
onChange | (value: XY<number>, state: DragState) => void | — |
onDragStart | (value: XY<number>, state: DragState) => void | — |
onDragEnd | (value: XY<number>, state: DragState) => void | — |
Full reference: UseDragValueOptions