gsap-draggable
Installation
SKILL.md
GSAP Draggable
Draggable enables drag, throw, rotation, and inertia-based interactions on any DOM element. Perfect for sliders, puzzles, games, and interactive UI components.
Installation
npm install gsap
import { Draggable } from 'gsap/Draggable'
import { InertiaPlugin } from 'gsap/InertiaPlugin'
gsap.registerPlugin(Draggable, InertiaPlugin)
Basic Drag
X/Y Drag
gsap.registerPlugin(Draggable)
// Enable drag on both axes
Draggable.create('.box', {
type: 'x,y',
bounds: '.container'
})
X Only
// Horizontal drag only
Draggable.create('.box', {
type: 'x',
bounds: '.container'
})
Y Only
// Vertical drag only
Draggable.create('.box', {
type: 'y',
bounds: '.container'
})
Left/Top Drag
// Use left/top instead of transform
Draggable.create('.box', {
type: 'left,top',
bounds: '.container'
})
Bounds
Container Bounds
Draggable.create('.box', {
type: 'x,y',
bounds: '.container' // Constrain to container
})
Rectangle Bounds
Draggable.create('.box', {
type: 'x,y',
bounds: {
minX: 0,
maxX: 500,
minY: 0,
maxY: 300
}
})
Partial Bounds
// Only constrain X
Draggable.create('.box', {
type: 'x,y',
bounds: {
minX: 100,
maxX: 400
}
})
Relative Bounds
// Relative to element's current position
Draggable.create('.box', {
type: 'x,y',
bounds: {
minX: '-=100', // 100px left of start
maxX: '+=100', // 100px right of start
minY: '-=50',
maxY: '+=50'
}
})
Rotation Bounds
Draggable.create('.knob', {
type: 'rotation',
bounds: {
minRotation: -135,
maxRotation: 135
}
})
Inertia (Throw)
Basic Inertia
gsap.registerPlugin(Draggable, InertiaPlugin)
Draggable.create('.box', {
type: 'x,y',
inertia: true // Enable throw
})
Inertia Resistance
Draggable.create('.box', {
type: 'x,y',
inertia: {
resistance: 2000 // Higher = stops faster
}
})
Inertia Duration
Draggable.create('.box', {
type: 'x,y',
inertia: {
minDuration: 0.5,
maxDuration: 2
}
})
Inertia Edge Resistance
Draggable.create('.box', {
type: 'x',
inertia: {
edgeResistance: 0.65 // Resistance at bounds (0-1)
}
})
Auto-scroll on Throw
Draggable.create('.box', {
type: 'x,y',
inertia: true,
edgeResistance: 0.8,
bounds: '#container',
autoScroll: 1 // Scroll container when hitting edge
})
Rotation
Basic Rotation
Draggable.create('.dial', {
type: 'rotation',
bounds: {
minRotation: 0,
maxRotation: 360
}
})
Circular Rotation
// Set transform origin for circular rotation
gsap.set('.dial', { transformOrigin: 'center center' })
Draggable.create('.dial', {
type: 'rotation'
})
Rotation with Snap
Draggable.create('.dial', {
type: 'rotation',
liveSnap: {
rotation: [0, 45, 90, 135, 180, 225, 270, 315, 360]
},
inertia: true
})
Rotation Stages
Draggable.create('.knob', {
type: 'rotation',
onDrag: function() {
// Snap to stages
const rotation = this.rotation
const stage = Math.round(rotation / 45) * 45
this.rotation = stage
}
})
Live Snap
Snap to Grid
Draggable.create('.box', {
type: 'x,y',
liveSnap: {
x: 20, // Snap every 20px
y: 20
}
})
Snap to Values
Draggable.create('.box', {
type: 'x',
liveSnap: {
x: [0, 100, 200, 300, 400]
}
})
Snap to Function
Draggable.create('.box', {
type: 'x',
liveSnap: {
x: value => {
// Snap to nearest multiple of 50
return Math.round(value / 50) * 50
}
}
})
Snap with Radius
Draggable.create('.box', {
type: 'x,y',
liveSnap: {
x: {
points: [0, 100, 200],
radius: 10 // Within 10px of point
}
}
})
Callbacks
Basic Callbacks
Draggable.create('.box', {
type: 'x,y',
// Lifecycle callbacks
onDragStart: function() {
console.log('Drag started')
},
onDrag: function() {
console.log('Dragging...', this.x, this.y)
},
onDragEnd: function() {
console.log('Drag ended')
},
onPress: function() {
console.log('Pressed')
},
onRelease: function() {
console.log('Released')
},
onClick: function() {
console.log('Clicked')
}
})
Access Data
Draggable.create('.box', {
type: 'x,y',
onDrag: function() {
// Access drag data
console.log(this.x) // Current X position
console.log(this.y) // Current Y position
console.log(this.deltaX) // Change since start
console.log(this.deltaY) // Change since start
console.log(this.startX) // Starting X position
console.log(this.startY) // Starting Y position
console.log(this.pointerX) // Pointer X position
console.log(this.pointerY) // Pointer Y position
}
})
Get Velocity
Draggable.create('.box', {
type: 'x,y',
inertia: true,
onDragEnd: function() {
const velocity = InertiaPlugin.getVelocity(this.target, 'x')
console.log('Throw velocity:', velocity)
}
})
Throw Callbacks
Draggable.create('.box', {
type: 'x,y',
inertia: true,
onThrowUpdate: function() {
console.log('Throwing...')
},
onThrowComplete: function() {
console.log('Throw complete')
}
})
Multiple Draggables
Array of Targets
// Create multiple draggable elements
Draggable.create(['.box1', '.box2', '.box3'], {
type: 'x,y',
bounds: '.container'
})
NodeList
// Create from NodeList
const boxes = document.querySelectorAll('.box')
Draggable.create(boxes, {
type: 'x,y',
bounds: '.container'
})
Individual Control
const draggables = Draggable.create('.box', {
type: 'x,y',
bounds: '.container'
})
// Access individual instance
dragga
bles[0].disable()
dragga
bles[1].enable()
Click Handling
Minimum Movement
Draggable.create('.box', {
type: 'x,y',
minimumMovement: 5, // Minimum pixels to register drag
onClick: function() {
console.log('Clicked without drag')
}
})
Clickable Elements
Draggable.create('.container', {
type: 'x,y',
dragClickables: false, // Don't drag clickable elements
bounds: '.container'
})
Drag vs Click
let isDragging = false
Draggable.create('.box', {
type: 'x,y',
onDragStart: function() {
isDragging = true
},
onDragEnd: function() {
isDragging = false
},
onClick: function() {
if (!isDragging) {
console.log('Pure click, no drag')
}
}
})
Advanced Patterns
Slider
Draggable.create('.handle', {
type: 'x',
bounds: {
minX: 0,
maxX: trackWidth - handleWidth
},
onDrag: function() {
const progress = this.x / (trackWidth - handleWidth)
gsap.to('.content', { x: -progress * contentWidth })
}
})
Sortable List
const items = document.querySelectorAll('.list-item')
Draggable.create(items, {
type: 'y',
bounds: '.list',
onDragEnd: function() {
// Get all items sorted by Y position
const sortedItems = Array.from(items)
.sort((a, b) => gsap.getProperty(a, 'y') - gsap.getProperty(b, 'y'))
// Reorder DOM
const list = document.querySelector('.list')
sortedItems.forEach(item => list.appendChild(item))
}
})
Puzzle Piece
Draggable.create('.puzzle-piece', {
type: 'x,y',
bounds: '.board',
inertia: true,
onPress: function() {
gsap.to(this.target, { scale: 1.1, duration: 0.2 })
},
onRelease: function() {
gsap.to(this.target, { scale: 1, duration: 0.2 })
// Check if near correct position
const correctX = 100
const correctY = 50
const dist = Math.sqrt(Math.pow(this.x - correctX, 2) + Math.pow(this.y - correctY, 2))
if (dist < 30) {
// Snap to correct position
gsap.to(this.target, {
x: correctX,
y: correctY,
duration: 0.3,
ease: 'elastic.out(1, 0.5)'
})
}
}
})
Swiper
const swiper = Draggable.create('.swiper', {
type: 'x',
bounds: {
minX: -(slideWidth * (totalSlides - 1)),
maxX: 0
},
inertia: true,
edgeResistance: 0.8,
onDragEnd: function() {
const snapIndex = Math.round(this.x / -slideWidth)
const snapX = snapIndex * -slideWidth
gsap.to(this.target, {
x: snapX,
duration: 0.5,
ease: 'power2.out'
})
}
})
Performance Tips
Use Transform Instead of Left/Top
// ❌ Slower, causes layout thrashing
Draggable.create('.box', {
type: 'left,top'
})
// ✅ Faster, GPU-accelerated
Draggable.create('.box', {
type: 'x,y'
})
Throttle Events
Draggable.create('.box', {
type: 'x,y',
onDrag: gsap.utils.throttle(function() {
console.log('Dragging...')
}, 100)
})
Batch Updates
let pendingUpdate = false
Draggable.create('.box', {
type: 'x,y',
onDrag: function() {
if (!pendingUpdate) {
pendingUpdate = true
requestAnimationFrame(() => {
// Batch update
console.log('Batched update')
pendingUpdate = false
})
}
}
})
Common Mistakes
1. Forgetting to Register InertiaPlugin
// ❌ Inertia won't work
gsap.registerPlugin(Draggable)
Draggable.create('.box', { inertia: true })
// ✅ Register both plugins
gsap.registerPlugin(Draggable, InertiaPlugin)
Draggable.create('.box', { inertia: true })
2. Wrong Transform Origin for Rotation
// ❌ Rotates from wrong point
Draggable.create('.dial', { type: 'rotation' })
// ✅ Set transform origin
gsap.set('.dial', { transformOrigin: 'center center' })
Draggable.create('.dial', { type: 'rotation' })
3. Bounds Not Working
// ❌ Container doesn't have position set
<div class="container">
<div class="box"></div>
</div>
Draggable.create('.box', { bounds: '.container' })
// ✅ Set position on container
.container { position: relative; }
.box { position: absolute; }
Best Practices
- Use x,y over left,top - Better performance
- Set position on bounds container - Required for bounds to work
- Use inertia wisely - Great for natural feel, but adds complexity
- Implement liveSnap - Better UX for constrained movements
- Handle click vs drag - Distinguish between user actions
- Use transformOrigin - Critical for rotation
- Test on mobile - Touch events can behave differently
- Clean up instances - Disable/kill when no longer needed
Quick Reference
| Feature | Method |
|---|---|
| Basic drag | Draggable.create(target, { type: 'x,y' }) |
| Bounds | bounds: '.container' |
| Inertia | inertia: true (needs InertiaPlugin) |
| Rotation | type: 'rotation' |
| Live snap | liveSnap: { x: 20 } |
| Callbacks | onDrag, onDragEnd, onClick |
| Get velocity | InertiaPlugin.getVelocity(target, 'x') |
| Minimum movement | minimumMovement: 5 |
| Edge resistance | edgeResistance: 0.8 |
| Auto scroll | autoScroll: 1 |
Weekly Installs
1
Repository
microck/gsap-skillsFirst Seen
6 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1