useDraggable
The useDraggable hook makes an element free-floating: it can be dragged anywhere in the viewport with a pointer, nudged with the arrow keys, and it stays on screen when the window is resized. Optionally it remembers where it was left.
It is browser-only — it is not exported from the native entry point.
import { useDraggable } from '@archibald/client';
const RETURN_VALUE = useDraggable(PARAMETERS);
Parameters
| Name | Type | Optional | Description |
|---|---|---|---|
| options | UseDraggableOptions | ✔️ | Configuration for placement, persistence and keys. |
options
UseDraggableOptions
| Property | Type | Description | Default |
|---|---|---|---|
| storageKey | string | Persist the position in local storage under this key. Omit to keep the placement for the page load only. | undefined |
| margin | number | Gap kept between the element and the viewport edges. | 16 |
| threshold | number | Pointer travel, in px, that separates a click from a drag. | 4 |
| keyStep | number | Distance an arrow key moves the element. | 10 |
| keyStepLarge | number | Distance an arrow key moves the element while shift is held. | 50 |
| corner | DraggableCorner | Where the element sits until it is moved: top-left, top-right, bottom-left or bottom-right. | bottom-right |
Return value
Draggable
| Property | Type | Description |
|---|---|---|
| ref | (node: HTMLElement | null) => void | Callback ref for the draggable element. A callback rather than a RefObject so the hook still measures correctly when the element mounts late (behind NoSSR, a portal or a lazy boundary). |
| position | DraggablePosition | null | The { x, y } to apply as left/top. null until the element has been measured. |
| isDragging | boolean | true while a drag gesture is in progress. |
| dragProps | object | onPointerDown and onKeyDown handlers to spread onto the element. |
| consumeDragged | () => boolean | true when the gesture that just ended was a drag, and resets on read. Use it to swallow the click that follows a drop. |
Usage
The element must be position: fixed and should set touch-action: none, so the browser does not claim the gesture for panning.
import { useDraggable } from '@archibald/client';
function FloatingButton({ onOpen }: { onOpen: () => void }) {
const { ref, position, isDragging, dragProps, consumeDragged } = useDraggable({
storageKey: 'my-app.floating-button'
});
return (
<button
ref={ref}
type="button"
className={isDragging ? 'floating floating--dragging' : 'floating'}
style={{ left: position?.x ?? 0, top: position?.y ?? 0, visibility: position ? 'visible' : 'hidden' }}
{...dragProps}
onClick={() => {
// Dropping the button fires a click; that one must not trigger the action.
if (consumeDragged()) {
return;
}
onOpen();
}}
>
Open
</button>
);
}
Behaviour notes
- Click vs drag. A gesture only counts as a drag once the pointer travels past
threshold, so a click with a shaky hand still activates the element. - Resizing. An element that has never been moved follows its
corner. Once it is placed by hand, that position is kept and only pulled back when the window shrinks past it. - Late styling. The element is re-settled whenever its own box changes, which covers stylesheets or fonts that land after the first layout pass and would otherwise leave it measured as 0 wide.
- Right-click. Only the primary button starts a gesture, so the context menu still works.