useDrag
useDrag is a Vue composable that tracks a pointer drag gesture (touch, mouse and pen) in real time, exposing the live offset, distance and direction.
Introduction
useDrag follows a pointer drag on an element and gives you the live offset, distance and direction, plus onStart / onMove / onEnd callbacks. It is the low-level gesture primitive behind components like MazBottomSheet (drag-to-dismiss), and it's useful to build sliders, sortable handles, swipeable cards, bottom sheets, custom carousels, etc.
It is built on top of Pointer Events, so it works with touch, mouse and pen.
TIP
For discrete directional swipe detection (left / right / up / down with a threshold) rather than real-time tracking, use useSwipe.
Key Features
- Real-time pointer tracking (touch, mouse and pen)
- Live reactive state:
isDragging,offsetX,offsetY,distance,direction onStart/onMove/onEndcallbacks with a full state snapshot- Axis locking (
x,yorboth) - Activation threshold, pointer-type filtering and reactive
disabled - Listeners are followed on the document, so the drag keeps working when the pointer leaves the element
- Automatically attaches/detaches when the target appears/disappears, and cleans up on unmount
Basic Usage
Drag the card down: it follows your pointer and snaps back on release.
<script lang="ts" setup>
import { useDrag } from 'maz-ui/composables'
import { ref } from 'vue'
const dragHandle = ref<HTMLElement>()
const dragOffset = ref(0)
const { isDragging: dragging, direction: dragDirection } = useDrag(dragHandle, {
axis: 'y',
onMove: ({ offsetY }) => {
dragOffset.value = Math.max(0, offsetY)
},
onEnd: ({ offsetY }) => {
if (offsetY > 150) {
// trigger an action when dragged far enough
}
dragOffset.value = 0
},
})
</script>
<template>
<div
ref="dragHandle"
class="drag-card"
:class="{ '--dragging': dragging }"
:style="{ transform: dragOffset ? `translateY(${dragOffset}px)` : undefined }"
>
{{ dragging ? `Dragging — ${Math.round(dragOffset)}px ${dragDirection ?? ''}` : 'Drag me down' }}
</div>
</template>
<style>
.drag-card {
touch-action: none;
cursor: grab;
user-select: none;
}
.drag-card:not(.--dragging) {
transition: transform 0.25s ease;
}
</style>Options
useDrag(target, options) - the target is a MaybeRefOrGetter<HTMLElement | string | null | undefined> and options accepts:
interface UseDragOptions {
/**
* Restrict the tracking to a single axis. Off-axis movement is ignored.
* @default 'both'
*/
axis?: 'x' | 'y' | 'both'
/**
* Minimum distance (px) the pointer must travel before the drag becomes active.
* @default 0
*/
threshold?: number
/**
* Pointer types allowed to initiate the drag.
* @default ['mouse', 'touch', 'pen']
*/
pointerTypes?: ('mouse' | 'touch' | 'pen')[]
/**
* Reactively disable the gesture.
* @default false
*/
disabled?: MaybeRefOrGetter<boolean>
/**
* Call `event.preventDefault()` on each pointer move while dragging.
* @default false
*/
preventDefault?: boolean
/**
* Call `event.stopPropagation()` on the pointer events.
* @default false
*/
stopPropagation?: boolean
/**
* Attach the listeners as soon as the target is available. When `false`, call `start()` manually.
* @default true
*/
immediate?: boolean
/** Called once when the drag becomes active (threshold reached). */
onStart?: (state: DragState) => void
/** Called on every pointer move while dragging. */
onMove?: (state: DragState) => void
/** Called when the drag ends (pointer up or cancel). */
onEnd?: (state: DragState) => void
}Each callback receives a DragState snapshot:
interface DragState {
/** Pointer offset from the drag start on the X axis (`0` when axis is `'y'`). */
offsetX: number
/** Pointer offset from the drag start on the Y axis (`0` when axis is `'x'`). */
offsetY: number
/** Absolute distance dragged from the start. */
distance: number
/** Dominant direction of the drag. */
direction: 'up' | 'down' | 'left' | 'right' | undefined
/** Pointer position when the drag started. */
startX: number
startY: number
/** Current pointer position. */
x: number
y: number
/** The underlying pointer event. */
event: PointerEvent
}Composable Return
interface UseDragReturn {
/** Whether a drag is currently active (threshold reached). */
isDragging: Readonly<Ref<boolean>>
/** Reactive pointer offset from the drag start on the X axis. */
offsetX: Readonly<Ref<number>>
/** Reactive pointer offset from the drag start on the Y axis. */
offsetY: Readonly<Ref<number>>
/** Reactive absolute distance dragged from the start. */
distance: Readonly<Ref<number>>
/** Reactive dominant direction of the drag. */
direction: Readonly<Ref<'up' | 'down' | 'left' | 'right' | undefined>>
/** Manually attach the listeners. */
start: () => void
/** Manually detach the listeners (also cancels an ongoing drag). */
stop: () => void
}Notes
- Add
touch-action: noneon the draggable element to prevent the browser from scrolling while dragging. - The composable attaches automatically on mount (
immediate: trueby default) and cleans up on unmount; usestart()/stop()for manual control. onEndonly fires when an actual drag happened (the threshold was reached), so a simple tap will not trigger it.- The target can be a ref, a getter or a CSS selector string, and the listeners re-attach automatically when it changes.