Skip to main content

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

NameTypeOptionalDescription
optionsUseDraggableOptions✔️Configuration for placement, persistence and keys.

options

UseDraggableOptions

PropertyTypeDescriptionDefault
storageKeystringPersist the position in local storage under this key. Omit to keep the placement for the page load only.undefined
marginnumberGap kept between the element and the viewport edges.16
thresholdnumberPointer travel, in px, that separates a click from a drag.4
keyStepnumberDistance an arrow key moves the element.10
keyStepLargenumberDistance an arrow key moves the element while shift is held.50
cornerDraggableCornerWhere the element sits until it is moved: top-left, top-right, bottom-left or bottom-right.bottom-right

Return value

Draggable

PropertyTypeDescription
ref(node: HTMLElement | null) => voidCallback 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).
positionDraggablePosition | nullThe { x, y } to apply as left/top. null until the element has been measured.
isDraggingbooleantrue while a drag gesture is in progress.
dragPropsobjectonPointerDown and onKeyDown handlers to spread onto the element.
consumeDragged() => booleantrue 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.