Skip to content

useDraggable

Makes any element draggable using Pointer Events. Returns Observable values for position (x$, y$), drag state (isDragging$), and a ready-to-use CSS style string (style$).

Basic
X only
Y only
Toggle

No active drag

Rose box is currently draggable
import {
function useRef$<T extends Element = Element>(externalRef?: React.Ref<T> | null): Ref$<T>

Creates an observable element ref. Can be used as a drop-in replacement for useRef, composed with callback refs, or used with forwardRef.

The element is wrapped with opaqueObject to prevent legendapp/state from making DOM properties reactive (deep observation).

@paramexternalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).

@returnsA callable ref that is also observable via get/peek

@example

// standalone — useRef replacement
const el$ = useRef$<HTMLDivElement>();
return <div ref={el$} />;
// forwardRef compatible
const Component = forwardRef<HTMLDivElement>((props, ref) => {
const el$ = useRef$(ref);
return <div ref={el$} />;
});
// callback ref composition
const myRef = useCallback((node: HTMLDivElement | null) => {
node?.focus();
}, []);
const el$ = useRef$(myRef);
return <div ref={el$} />;

useRef$
,
import useDraggable
useDraggable
} from "@usels/core";
function
function DraggableBox(): React.JSX.Element
DraggableBox
() {
const
const el$: Ref$<HTMLDivElement>
el$
=
useRef$<HTMLDivElement>(externalRef?: React.Ref<HTMLDivElement> | undefined): Ref$<HTMLDivElement>

Creates an observable element ref. Can be used as a drop-in replacement for useRef, composed with callback refs, or used with forwardRef.

The element is wrapped with opaqueObject to prevent legendapp/state from making DOM properties reactive (deep observation).

@paramexternalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).

@returnsA callable ref that is also observable via get/peek

@example

// standalone — useRef replacement
const el$ = useRef$<HTMLDivElement>();
return <div ref={el$} />;
// forwardRef compatible
const Component = forwardRef<HTMLDivElement>((props, ref) => {
const el$ = useRef$(ref);
return <div ref={el$} />;
});
// callback ref composition
const myRef = useCallback((node: HTMLDivElement | null) => {
node?.focus();
}, []);
const el$ = useRef$(myRef);
return <div ref={el$} />;

useRef$
<
interface HTMLDivElement
HTMLDivElement
>();
const {
const x$: any
x$
,
const y$: any
y$
} =
import useDraggable
useDraggable
(
const el$: Ref$<HTMLDivElement>
el$
);
return (
<
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
React.RefAttributes<HTMLDivElement>.ref?: React.Ref<HTMLDivElement> | undefined

Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref).

ref
={
const el$: Ref$<HTMLDivElement>
el$
}
React.HTMLAttributes<HTMLDivElement>.style?: React.CSSProperties | undefined
style
={{
StandardLonghandProperties<string | number, string & {}>.position?: Property.Position | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: static | relative | absolute | sticky | fixed

Initial value: static

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :---: | | 1 | 1 | 1 | 12 | 4 |

position
: "fixed",
StandardLonghandProperties<string | number, string & {}>.left?: Property.Left<string | number> | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: auto | <length-percentage> | <anchor()> | <anchor-size()>

Initial value: auto

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :-----: | | 1 | 1 | 1 | 12 | 5.5 |

left
: `${
const x$: any
x$
.
any
get
()}px`,
StandardLonghandProperties<string | number, string & {}>.top?: Property.Top<string | number> | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: auto | <length-percentage> | <anchor()> | <anchor-size()>

Initial value: auto

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :---: | | 1 | 1 | 1 | 12 | 5 |

top
: `${
const y$: any
y$
.
any
get
()}px` }}>
Drag me
</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
);
}

Restrict movement to a single axis.

const { x$ } = useDraggable(el$, { axis: "x" }); // horizontal only
const { y$ } = useDraggable(el$, { axis: "y" }); // vertical only

Attach drag to a specific handle element instead of the whole target.

import {
function useRef$<T extends Element = Element>(externalRef?: React.Ref<T> | null): Ref$<T>

Creates an observable element ref. Can be used as a drop-in replacement for useRef, composed with callback refs, or used with forwardRef.

The element is wrapped with opaqueObject to prevent legendapp/state from making DOM properties reactive (deep observation).

@paramexternalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).

@returnsA callable ref that is also observable via get/peek

@example

// standalone — useRef replacement
const el$ = useRef$<HTMLDivElement>();
return <div ref={el$} />;
// forwardRef compatible
const Component = forwardRef<HTMLDivElement>((props, ref) => {
const el$ = useRef$(ref);
return <div ref={el$} />;
});
// callback ref composition
const myRef = useCallback((node: HTMLDivElement | null) => {
node?.focus();
}, []);
const el$ = useRef$(myRef);
return <div ref={el$} />;

useRef$
,
import useDraggable
useDraggable
} from "@usels/core";
function
function WithHandle(): void
WithHandle
() {
const
const el$: Ref$<HTMLDivElement>
el$
=
useRef$<HTMLDivElement>(externalRef?: React.Ref<HTMLDivElement> | undefined): Ref$<HTMLDivElement>

Creates an observable element ref. Can be used as a drop-in replacement for useRef, composed with callback refs, or used with forwardRef.

The element is wrapped with opaqueObject to prevent legendapp/state from making DOM properties reactive (deep observation).

@paramexternalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).

@returnsA callable ref that is also observable via get/peek

@example

// standalone — useRef replacement
const el$ = useRef$<HTMLDivElement>();
return <div ref={el$} />;
// forwardRef compatible
const Component = forwardRef<HTMLDivElement>((props, ref) => {
const el$ = useRef$(ref);
return <div ref={el$} />;
});
// callback ref composition
const myRef = useCallback((node: HTMLDivElement | null) => {
node?.focus();
}, []);
const el$ = useRef$(myRef);
return <div ref={el$} />;

useRef$
<
interface HTMLDivElement
HTMLDivElement
>();
const
const handle$: Ref$<HTMLDivElement>
handle$
=
useRef$<HTMLDivElement>(externalRef?: React.Ref<HTMLDivElement> | undefined): Ref$<HTMLDivElement>

Creates an observable element ref. Can be used as a drop-in replacement for useRef, composed with callback refs, or used with forwardRef.

The element is wrapped with opaqueObject to prevent legendapp/state from making DOM properties reactive (deep observation).

@paramexternalRef - Optional. Accepts callback ref, RefObject, or null (forwardRef compatible).

@returnsA callable ref that is also observable via get/peek

@example

// standalone — useRef replacement
const el$ = useRef$<HTMLDivElement>();
return <div ref={el$} />;
// forwardRef compatible
const Component = forwardRef<HTMLDivElement>((props, ref) => {
const el$ = useRef$(ref);
return <div ref={el$} />;
});
// callback ref composition
const myRef = useCallback((node: HTMLDivElement | null) => {
node?.focus();
}, []);
const el$ = useRef$(myRef);
return <div ref={el$} />;

useRef$
<
interface HTMLDivElement
HTMLDivElement
>();
const {
const style$: any
style$
} =
import useDraggable
useDraggable
(
const el$: Ref$<HTMLDivElement>
el$
, {
handle: Ref$<HTMLDivElement>
handle
:
const handle$: Ref$<HTMLDivElement>
handle$
});
// ...
}

Clamp drag position inside a container element.

const { style$ } = useDraggable(el$, {
containerElement: container$,
});
const { style$ } = useDraggable(el$, {
restrictInView: true,
});
const { isDragging$ } = useDraggable(el$, {
onStart: (pos, e) => {
if (someCondition) return false; // cancel drag
},
onMove: (pos, e) => console.log(pos),
onEnd: (pos, e) => savePosition(pos),
});

x$ and y$ are writable Observables — setting them directly updates style$ and position$.

const { x$, y$, style$, position$ } = useDraggable(el$);
x$.set(100);
y$.set(200);
// style$.get() === 'left: 100px; top: 200px;'
export interface Position {
x: number;
y: number;
}
export interface UseDraggableOptions {
exact?: boolean;
preventDefault?: boolean;
stopPropagation?: boolean;
capture?: boolean;
handle?: MaybeElement;
containerElement?: MaybeElement;
initialValue?: Position;
onStart?: (position: Position, event: PointerEvent) => void | false;
onMove?: (position: Position, event: PointerEvent) => void;
onEnd?: (position: Position, event: PointerEvent) => void;
axis?: "x" | "y" | "both";
disabled?: boolean;
pointerTypes?: Array<"mouse" | "pen" | "touch">;
restrictInView?: boolean;
}
export interface UseDraggableReturn {
x$: Observable<number>;
y$: Observable<number>;
position$: Observable<Position>;
isDragging$: Observable<boolean>;
style$: Observable<string>;
}
export declare function useDraggable(target: MaybeElement, options?: DeepMaybeObservable<UseDraggableOptions>): UseDraggableReturn;

View on GitHub

  • tigerwest
  • a7392ab 2026-03-06 - feat(core,browser): add sync strategy hooks (tigerwest)