Skip to main content
Motion provides powerful drag gesture support with physics-based momentum, constraints, and elastic edges.

Basic Drag

Enable dragging with the drag prop:
<motion.div drag />
This allows free dragging in both x and y directions.

Axis Locking

Constrain dragging to a single axis:
<motion.div drag="x" />  {/* Horizontal only */}
<motion.div drag="y" />  {/* Vertical only */}

Drag Constraints

Limit drag movement with pixel or ref-based constraints:

Pixel Constraints

<motion.div
  drag
  dragConstraints={{
    left: 0,
    right: 100,
    top: 0,
    bottom: 100
  }}
/>

Ref-Based Constraints

Constrain within a parent element:
import { useRef } from "react"

export function ConstrainedDrag() {
  const constraintsRef = useRef(null)
  
  return (
    <div ref={constraintsRef} style={{ width: 400, height: 400 }">
      <motion.div
        drag
        dragConstraints={constraintsRef}
      />
    </div>
  )
}
From VisualElementDragControls.ts:351-354, Motion handles ref constraints:
if (dragConstraints && isRefObject(dragConstraints)) {
  if (!this.constraints) {
    this.constraints = this.resolveRefConstraints()
  }
}

Elastic Edges

Control the “bounce” at constraint edges:
<motion.div
  drag
  dragConstraints={{ left: 0, right: 100 }}
  dragElastic={0.1}  // 0 = no elastic, 1 = very elastic
/>
Disable elastic edges entirely:
<motion.div
  drag
  dragConstraints={{ left: 0, right: 100 }}
  dragElastic={0}  // Completely rigid edges
/>
From VisualElementDragControls.ts:470-471, elastic values affect spring physics:
const bounceStiffness = dragElastic ? 200 : 1000000
const bounceDamping = dragElastic ? 40 : 10000000

Momentum and Inertia

Control post-drag momentum:
<motion.div
  drag
  dragMomentum={true}     // Enable momentum (default)
  dragTransition={{
    bounceStiffness: 200,   // Spring stiffness at edges
    bounceDamping: 40,      // Spring damping
    timeConstant: 750       // Momentum decay time
  }}
/>
Disable momentum for instant stop:
<motion.div
  drag
  dragMomentum={false}
/>

Direction Lock

Lock to the first detected drag direction:
<motion.div
  drag
  dragDirectionLock
  onDirectionLock={(axis) => console.log(axis)}  // "x" or "y"
/>
From VisualElementDragControls.ts:195-204:
if (dragDirectionLock && this.currentDirection === null) {
  this.currentDirection = getCurrentDirection(offset)
  
  if (this.currentDirection !== null) {
    onDirectionLock && onDirectionLock(this.currentDirection)
  }
  
  return
}

Drag Events

Respond to drag lifecycle:
<motion.div
  drag
  onDragStart={(event, info) => {
    console.log(info.point.x, info.point.y)
  }}
  onDrag={(event, info) => {
    console.log(info.offset.x, info.offset.y)
  }}
  onDragEnd={(event, info) => {
    console.log(info.velocity.x, info.velocity.y)
  }}
/>

Info Object

interface PanInfo {
  point: { x: number, y: number }      // Current pointer position
  delta: { x: number, y: number }      // Change since last event
  offset: { x: number, y: number }     // Total movement from origin
  velocity: { x: number, y: number }   // Velocity in px/s
}

Drag Controls

Programmatically control drag with useDragControls:
import { motion, useDragControls } from "motion/react"

export function DragHandle() {
  const controls = useDragControls()
  
  return (
    <div>
      <div 
        onPointerDown={(e) => controls.start(e)}
        style={{ cursor: "grab" }}
      >
        Drag Handle
      </div>
      <motion.div
        drag
        dragControls={controls}
        dragListener={false}  // Disable automatic drag
      />
    </div>
  )
}

Snap to Cursor

Snap element to cursor position on drag start:
const controls = useDragControls()

function startDrag(e) {
  controls.start(e, { snapToCursor: true })
}
From VisualElementDragControls.ts:111-113:
const onSessionStart = (event: PointerEvent) => {
  if (snapToCursor) {
    this.snapToCursor(extractEventInfo(event).point)
  }
}

Real-World Examples

Draggable Card

import { motion } from "motion/react"

export function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{
        left: -100,
        right: 100,
        top: -100,
        bottom: 100
      }}
      dragElastic={0.1}
      whileDrag={{ scale: 1.05, cursor: "grabbing" }}
      style={{
        width: 200,
        height: 200,
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        borderRadius: 20,
        cursor: "grab"
      }}
    >
      Drag me!
    </motion.div>
  )
}

Slider Control

import { useState } from "react"
import { motion } from "motion/react"

export function Slider() {
  const [value, setValue] = useState(50)
  
  return (
    <div style={{ width: 300, padding: 20 }">
      <div style={{ position: "relative", height: 4, background: "#ddd" }">
        <motion.div
          drag="x"
          dragConstraints={{ left: 0, right: 300 }}
          dragElastic={0}
          dragMomentum={false}
          onDrag={(e, info) => {
            setValue(Math.round((info.point.x / 300) * 100))
          }}
          style={{
            width: 20,
            height: 20,
            borderRadius: "50%",
            background: "#0070f3",
            position: "absolute",
            top: -8,
            x: value * 3
          }}
        />
      </div>
      <p>Value: {value}</p>
    </div>
  )
}

Drag to Dismiss

import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"

export function DismissibleCard() {
  const [items, setItems] = useState([1, 2, 3])
  
  return (
    <AnimatePresence>
      {items.map(item => (
        <motion.div
          key={item}
          drag="x"
          dragConstraints={{ left: 0, right: 0 }}
          dragElastic={1}
          onDragEnd={(e, info) => {
            if (Math.abs(info.offset.x) > 100) {
              setItems(items.filter(i => i !== item))
            }
          }}
          exit={{ opacity: 0, height: 0 }}
          style={{
            padding: 20,
            margin: 10,
            background: "white",
            borderRadius: 8
          }}
        >
          Swipe to dismiss
        </motion.div>
      ))}
    </AnimatePresence>
  )
}

Constrained in Rotated Parent

From the actual codebase example Drag-draggable.tsx:
import { motion } from "motion/react"
import { useRef } from "react"

export function ConstrainedDrag() {
  const ref = useRef(null)
  
  return (
    <motion.div
      drag
      dragConstraints={{ left: 0, right: 100, top: 0, bottom: 100 }}
      ref={ref}
      dragElastic={0}
      whileTap={{ scale: 0.95 }}
      style={{
        width: 200,
        height: 200,
        background: "white",
        borderRadius: 20
      }}
    />
  )
}

Drag Propagation

Control whether child drags affect parent:
<motion.div drag>
  <motion.div 
    drag
    dragPropagation  // Allow parent to receive drag events
  />
</motion.div>

Blocking Text Selection

Motion automatically handles text input elements. From VisualElementDragControls.ts:667-669:
const isClickingTextInputChild =
  target !== element && isElementTextInput(target)

if (drag && dragListener && !isClickingTextInputChild) {
  this.start(event)
}
This prevents drag from interfering with input, textarea, select, and contenteditable elements.

Layout-Aware Drag

Combine drag with layout animations:
<motion.div
  layout
  drag
  dragConstraints={parentRef}
/>
From VisualElementDragControls.ts:728-737, drag automatically compensates for layout changes:
if (this.isDragging && hasLayoutChanged) {
  eachAxis((axis) => {
    const motionValue = this.getAxisMotionValue(axis)
    if (!motionValue) return
    
    this.originPoint[axis] += delta[axis].translate
    motionValue.set(motionValue.get() + delta[axis].translate)
  })
}

Snap to Origin

Return to starting position after drag:
<motion.div
  drag
  dragSnapToOrigin
  dragTransition={{ bounceStiffness: 200, bounceDamping: 40 }}
/>
From VisualElementDragControls.ts:238-241:
const resumeAnimation = () => {
  const { dragSnapToOrigin: snap } = this.getProps()
  if (snap || this.constraints) {
    this.startAnimation({ x: 0, y: 0 })
  }
}

Performance Tips

Use dragElastic={0} for UI controls - Eliminates elastic calculation overhead.
Disable dragMomentum for precise controls - Reduces physics calculations for sliders and handles.
Add will-change: transform - Motion does this automatically, but can be added manually for complex scenarios.

Accessibility

Make draggable elements keyboard accessible:
<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 100 }}
  tabIndex={0}
  role="slider"
  aria-valuemin={0}
  aria-valuemax={100}
  aria-valuenow={value}
  onKeyDown={(e) => {
    if (e.key === 'ArrowRight') {
      // Handle keyboard interaction
    }
  }}
/>

Next Steps

Spring Animations

Fine-tune drag physics

Layout Transitions

Combine drag with layout

Performance

Optimize drag performance

Accessibility

Make drag accessible

Build docs developers (and LLMs) love