pota/use/dragdraggable is a ref factory that turns an element into a drag handle
and reports cumulative deltas and pointer position. onMove is called
on every pointermove between pointerdown and
pointerup/pointercancel, with the cumulative delta from the
starting position.
pointermove, pointerup, and pointercancel are listened for on
document, so the gesture continues even when the pointer leaves the
handle. The element's bounding rect is snapshotted once per gesture,
so element-relative coordinates stay meaningful even if the element
moves while dragging.
Use dx/dy when you're moving the element itself. Use
elementX/elementY or percentX/percentY for sliders, range
pickers, and color canvases — where the element doesn't move during
the gesture and the pointer position within its bounds is what
matters.
draggable takes a single options object and returns a ref function
for use:ref.
| Option | Type | Description |
|---|---|---|
onMove |
(info: DragInfo) => void |
Required. Fires on every pointermove during a drag gesture. |
onStart |
(info: DragInfo) => void |
Optional. Fires once on pointerdown when a gesture begins. |
onEnd |
(info: DragInfo) => void |
Optional. Fires on pointerup/pointercancel when a gesture ends. |
Returns: a ref function — (node: HTMLElement) => void — to pass
to use:ref.
Each callback receives a DragInfo object:
| Field | Description |
|---|---|
dx, dy |
Cumulative delta from the pointer position at pointerdown. |
x, y |
Current pointer position (clientX / clientY). |
originX, originY |
Pointer position at pointerdown. |
elementX, elementY |
Pointer position relative to the element's box at gesture start, clamped to the box. |
percentX, percentY |
elementX / elementY expressed as 0–100. |
event |
The raw PointerEvent. |
Drive an absolutely-positioned element from dx/dy deltas. Track
the element's last commit on onEnd so the next drag starts where the
previous one left off.
import { render, signal } from 'pota'
import { draggable } from 'pota/use/drag'
function App() {
const pos = signal({ x: 40, y: 40 })
let committed = { x: 40, y: 40 }
return (
<div
use:ref={draggable({
onMove(info) {
pos.write({
x: committed.x + info.dx,
y: committed.y + info.dy,
})
},
onEnd() {
committed = pos.read()
},
})}
style={() => ({
position: 'absolute',
left: pos.read().x + 'px',
top: pos.read().y + 'px',
width: '120px',
height: '120px',
background: 'rebeccapurple',
color: 'white',
'border-radius': '8px',
display: 'grid',
'place-items': 'center',
cursor: 'grab',
'user-select': 'none',
'touch-action': 'none',
})}
>
drag me
</div>
)
}
render(App)
Use percentX to build a horizontal slider. The element doesn't move;
the pointer position within its bounds drives the value.
import { render, signal } from 'pota'
import { draggable } from 'pota/use/drag'
function App() {
const value = signal(50)
return (
<div style={{ width: '240px', 'user-select': 'none' }}>
<div
use:ref={draggable({
onMove(info) {
value.write(Math.round(info.percentX))
},
})}
style={{
position: 'relative',
height: '24px',
background: '#eee',
'border-radius': '12px',
cursor: 'pointer',
'touch-action': 'none',
}}
>
<div
style={() => ({
position: 'absolute',
top: '0',
left: '0',
height: '100%',
width: value.read() + '%',
background: 'rebeccapurple',
'border-radius': '12px',
})}
/>
</div>
<p>{() => value.read() + '%'}</p>
</div>
)
}
render(App)