draggable
Make an element draggable with Pointer Events.
| Package | @vizejs/composable/draggable |
| Own the source | vize lib pull composable:draggable |
| Runtime exports | useDraggable |
| Gzip budget | 3840 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { useDraggable } from "@vizejs/composable/draggable";
Try it
Move a local review note inside native bounds
Use a separate pointer handle, reactive axis and disabled state while keeping a complete note inside an actual element rectangle.
What to observe: Drag the handle toward the board edges: the whole note remains bounded, moves continue beyond the handle and Start/End counts describe actual drags. Select X only or lock pointer dragging. Edit the local note and move it with the keyboard buttons; the text stays exact. Stop listeners: pointer dragging stops while the explicit placement buttons still work.
useDraggable exposes writable client-coordinate x/y and a style for fixed or absolute placement; this sample uses fixed positioning within its iframe. The real board ref supplies bounds, and the note's own dimensions are subtracted at the right/bottom edges. Axis and disabled affect pointer gestures, not arbitrary assignments to writable refs: the independent keyboard placement function explicitly clamps its writes. The chosen SSR position is a fallback; mounting measures the real board and places the same note inside it. A left mouse pointer starts the drag, and owner-document move/up/cancel listeners follow its pointer identifier. Stop is idempotent and scope disposal removes listeners; this example does not claim tested pen/touch gestures or persist the draft.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { onMounted, ref, useTemplateRef } from "vue";
import { useDraggable } from "@vizejs/composable/draggable";
import type { DragAxis } from "@vizejs/composable/draggable";
const board = useTemplateRef<HTMLElement>("board");
const note = useTemplateRef<HTMLElement>("note");
const handle = useTemplateRef<HTMLButtonElement>("handle");
const axis = ref<DragAxis>("both");
const locked = ref(false);
const stopped = ref(false);
const draft = ref("Review the native preview");
const starts = ref(0);
const ends = ref(0);
const { x, y, style, isDragging, stop } = useDraggable(note, {
handle,
bounds: board,
axis,
disabled: locked,
preventDefault: true,
initialValue: { x: 24, y: 160 },
onStart: () => {
starts.value += 1;
},
onEnd: () => {
ends.value += 1;
},
});
function placeNote(dx: number, dy: number, reset = false): void {
const bounds = board.value?.getBoundingClientRect();
const size = note.value?.getBoundingClientRect();
if (!bounds || !size) return;
x.value = Math.min(
Math.max(bounds.left, reset ? bounds.left + 12 : x.value + dx),
Math.max(bounds.left, bounds.right - size.width),
);
y.value = Math.min(
Math.max(bounds.top, reset ? bounds.top + 12 : y.value + dy),
Math.max(bounds.top, bounds.bottom - size.height),
);
}
function stopDragging(): void {
stop();
stopped.value = true;
}
onMounted(() => placeNote(0, 0, true));
</script>
<template>
<div class="composable-example">
<div class="drag-controls">
<label>Note text <input v-model="draft" /></label>
<label>
Drag axis
<select v-model="axis">
<option value="both">Both axes</option>
<option value="x">X only</option>
<option value="y">Y only</option>
</select>
</label>
<label class="drag-lock"
><input v-model="locked" type="checkbox" /> Lock pointer dragging</label
>
</div>
<section ref="board" class="drag-board" aria-label="Bounded note board">
<article ref="note" class="drag-note" :style aria-label="Draggable review note">
<button ref="handle" type="button" aria-label="Drag review note">Drag note</button>
<span>{{ draft }}</span>
</article>
</section>
<div class="example-actions">
<button type="button" @click="() => placeNote(24, 0)">Move note right</button>
<button type="button" @click="() => placeNote(0, 24)">Move note down</button>
<button type="button" @click="() => placeNote(0, 0, true)">Reset note position</button>
<button type="button" :disabled="stopped" @click="stopDragging">Stop drag listeners</button>
</div>
<output aria-live="polite">
Position: {{ Math.round(x) }}, {{ Math.round(y) }} · Dragging:
{{ isDragging ? "yes" : "no" }} · Starts: {{ starts }} · Ends: {{ ends }}
</output>
<details>
<summary>How pointer placement works</summary>
<p>
Drag the handle; moves continue on the owner document and keep the whole note inside the
board. Axis and Lock constrain pointer dragging. The keyboard buttons place the note
independently and explicitly apply the same bounds, including after listeners stop.
</p>
<p>
Position uses this iframe's client coordinates and fixed positioning. Server rendering uses
the chosen 24, 160 fallback; mounting places it inside the actual board. The local draft
survives movement. Bounds are read during pointer moves; Reset places the note again after
scrolling or layout changes. Nothing is saved or sent.
</p>
</details>
</div>
</template>
<style scoped>
.drag-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: end;
label {
display: grid;
gap: 4px;
flex: 1 1 120px;
min-inline-size: 0;
}
.drag-lock {
display: flex;
align-items: center;
flex-basis: 180px;
input {
inline-size: auto;
}
}
}
.drag-board {
block-size: 160px;
border: 1px dashed currentColor;
}
.drag-note {
--note-surface: Canvas;
--note-layer: 1;
position: fixed;
z-index: var(--note-layer);
box-sizing: border-box;
inline-size: 160px;
block-size: 120px;
padding: 8px;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
border: 1px solid currentColor;
border-radius: 8px;
background: var(--note-surface);
overflow-wrap: anywhere;
button {
touch-action: none;
cursor: grab;
}
span {
min-block-size: 0;
overflow: auto;
}
}
</style>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
useDraggable |
input | experimental | deterministic-fallback | stable | caller, reactive-scope | web, desktop | — | resolveElement, isElementNode, toPointerKind, tryOnScopeDispose |
API
useDraggable
Make an element draggable with Pointer Events. pointerdown on the handle records the offset between the pointer and the element; moves are tracked on the owner document (so fast drags that leave the element keep working) and constrained by axis and bounds. x/y are writable for programmatic placement. Server renders expose initialValue; listeners are removed with the owning reactive scope.
function useDraggable( target: MaybeElementTarget, options: UseDraggableOptions = {}, ): DraggableControls
Types
DragBounds
Rectangle (in the same coordinate space as the position) that bounds dragging.
| Member | Type | Description |
|---|---|---|
left |
number |
Minimum x. |
top |
number |
Minimum y. |
right |
number |
Maximum x of the dragged element's right edge. |
bottom |
number |
Maximum y of the dragged element's bottom edge. |
UseDraggableOptions
Options for useDraggable.
| Member | Type | Description |
|---|---|---|
handle? |
MaybeElementTarget |
Element that starts the drag. Defaults to the dragged element itself. |
axis? |
MaybeRefOrGetter<DragAxis> |
Restrict movement to one axis. |
bounds? |
MaybeRefOrGetter<DragBounds | null | undefined> | MaybeElementTarget |
Keep the element inside these bounds, or inside a container element's client rectangle. |
initialValue? |
Point |
Position before the first drag and during server rendering. |
pointerTypes? |
readonly PointerKind[] |
Device kinds that may start a drag. |
exact? |
boolean |
Only start when the pointer goes down on the handle itself, not a descendant. |
preventDefault? |
boolean |
Call preventDefault() on pointer events while dragging. |
disabled? |
MaybeRefOrGetter<boolean> |
Temporarily disable dragging. |
onStart? |
(position: Point, event: PointerEvent) => boolean | void |
Called before a drag starts. Return false to cancel it. |
onMove? |
(position: Point, event: PointerEvent) => void |
Called after every position update. |
onEnd? |
(position: Point, event: PointerEvent) => void |
Called when the drag ends. |
DraggableControls
Reactive drag state returned by useDraggable.
| Member | Type | Description |
|---|---|---|
x |
Ref<number> |
Horizontal position (the element's left edge in client coordinates). |
y |
Ref<number> |
Vertical position (the element's top edge in client coordinates). |
position |
ComputedRef<Point> |
Current position snapshot. |
isDragging |
Readonly<Ref<boolean>> |
Whether a drag is in progress. |
style |
ComputedRef<string> |
left/top declaration for position: fixed or absolute placement. |
stop |
() => void |
Remove listeners. Idempotent. |