InteractJS is a JavaScript library for drag and drop, resizing, and multi-touch gestures for modern browsers. It provides powerful options like inertia and modifiers for snapping and restricting element movements.
Official Documentation: https://interactjs.io/docs/
GitHub Repository: https://github.com/taye/interact.js
Install the pre-bundled package with all features:
npm install --save interactjsImport in your JavaScript:
// ES6 import
import interact from 'interactjs'
// CommonJS or AMD
const interact = require('interactjs')<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/interactjs/dist/interact.min.js"></script>Install only the features you need:
npm install --save @interactjs/interact \
@interactjs/auto-start \
@interactjs/actions \
@interactjs/modifiers \
@interactjs/dev-toolsimport '@interactjs/auto-start'
import '@interactjs/actions/drag'
import '@interactjs/actions/resize'
import '@interactjs/modifiers'
import '@interactjs/dev-tools'
import interact from '@interactjs/interact'
interact('.item').draggable({
listeners: {
move(event) {
console.log(event.pageX, event.pageY)
},
},
})| Package | Description |
|---|---|
| @interactjs/interact | (required) provides the interact() method |
| @interactjs/actions | Drag, resize, gesture actions |
| @interactjs/auto-start | Start actions with pointer down, move sequence |
| @interactjs/modifiers | Snap, restrict, etc. modifiers |
| @interactjs/snappers | Provides interact.snappers.grid() utility |
| @interactjs/inertia | Drag and resize inertia-like throwing |
| @interactjs/reflow | interactable.reflow(action) method |
| @interactjs/dev-tools | Console warnings for common mistakes |
<script type="module">
import 'https://cdn.interactjs.io/v1.9.20/auto-start/index.js'
import 'https://cdn.interactjs.io/v1.9.20/actions/drag/index.js'
import 'https://cdn.interactjs.io/v1.9.20/actions/resize/index.js'
import 'https://cdn.interactjs.io/v1.9.20/modifiers/index.js'
import 'https://cdn.interactjs.io/v1.9.20/dev-tools/index.js'
import interact from 'https://cdn.interactjs.io/v1.9.20/interactjs/index.js'
interact('.item').draggable({
onmove(event) {
console.log(event.pageX, event.pageY)
},
})
</script>If using the library only through CDN:
npm install --save-dev @interactjs/typesThe basic steps to setting up interactions:
- Create an
Interactabletarget - Configure it to enable actions and add modifiers, inertia, etc.
- Add event listeners to provide visual feedback and update your app's state
// Step 1: Target elements with the "slider" class
const slider = interact('.slider')
slider
// Step 2: Configure the draggable action
.draggable({
origin: 'self', // (0, 0) will be the element's top-left
inertia: true, // start inertial movement if thrown
modifiers: [
interact.modifiers.restrict({
restriction: 'self', // keep the drag coords within the element
}),
],
})
// Step 3: Add event listener
.on('dragmove', function (event) {
const sliderWidth = interact.getElementRect(event.target.parentNode).width
const value = event.pageX / sliderWidth
event.target.style.paddingLeft = (value * 100) + '%'
event.target.setAttribute('data-value', value.toFixed(2))
})- 🖱️ Draggable - Move elements or draw on canvas with drag events
- 📐 Resizable - Resize elements by dragging edges or corners
- 👆 Gesturable - Multi-touch gestures with angle, scale, and rotation data
- 📍 Dropzones - Define drop targets for drag and drop applications
- ⚡ Inertia - Physics-based momentum after releasing elements
- 🎯 Snapping - Snap to grid, points, or custom targets
- 🔒 Restriction - Constrain movement within boundaries
- 🎛️ Modifiers - Customize event coordinates with modifiers
Make elements draggable by creating an interactable and calling the draggable method.
const position = { x: 0, y: 0 }
interact('.draggable').draggable({
listeners: {
start(event) {
console.log(event.type, event.target)
},
move(event) {
position.x += event.dx
position.y += event.dy
event.target.style.transform = `translate(${position.x}px, ${position.y}px)`
},
}
})<div class="draggable">Draggable Element</div>.draggable {
touch-action: none;
user-select: none;
}Important: Use CSS touch-action: none to prevent browser panning on touch devices, and user-select: none to disable text selection.
| Property | Description |
|---|---|
| dragEnter | The dropzone this Interactable was dragged over |
| dragLeave | The dropzone this Interactable was dragged out of |
// Lock the drag to the starting direction
interact(singleAxisTarget).draggable({
startAxis: 'xy',
lockAxis: 'start'
})
// Only drag if started horizontally
interact(horizontalTarget).draggable({
startAxis: 'x',
lockAxis: 'x'
})- startAxis: Sets the direction that initial movement must be in for action to start. Use
'x'for horizontal or'y'for vertical. - lockAxis: Causes drag events to change only in the given axis. Use
'start'to lock to starting direction.
Resize elements by dragging their edges.
interact('.resizable').resizable({
edges: { top: true, left: true, bottom: true, right: true },
listeners: {
move(event) {
let { x, y } = event.target.dataset
x = (parseFloat(x) || 0) + event.deltaRect.left
y = (parseFloat(y) || 0) + event.deltaRect.top
Object.assign(event.target.style, {
width: `${event.rect.width}px`,
height: `${event.rect.height}px`,
transform: `translate(${x}px, ${y}px)`,
})
Object.assign(event.target.dataset, { x, y })
},
},
})<div data-x="0" data-y="0" class="resizable">
<!-- top-left resize handle -->
<div class="resize-top resize-left"></div>
<!-- bottom-right resize handle -->
<div class="resize-bottom resize-right"></div>
</div>.resizable {
touch-action: none;
user-select: none;
box-sizing: border-box;
}| Property | Description |
|---|---|
| edges | The edges of the element being changed |
| rect | Object with new dimensions of the target |
| deltaRect | Change in dimensions since the previous event |
The edges property specifies which edges can be resized:
interact(target).resizable({
edges: {
top: true, // Use pointer coords to check for resize
left: false, // Disable resizing from left edge
bottom: '.resize-s', // Resize if pointer target matches selector
right: handleEl // Resize if pointer target is the given Element
}
})Controls behavior when resizing would make dimensions less than 0x0:
interact(target).resizable({
edges: { bottom: true, right: true },
invert: 'reposition' // 'none' | 'negate' | 'reposition'
})| Value | Description |
|---|---|
| 'none' | (default) Limit resize rect to minimum 0x0 |
| 'negate' | Allow rect to have negative width/height |
| 'reposition' | Keep positive dimensions by swapping edges |
Maintain aspect ratio while resizing:
interact(target).resizable({
modifiers: [
interact.modifiers.aspectRatio({
ratio: 2, // width always double the height, or 'preserve'
modifiers: [
interact.modifiers.restrictSize({ max: 'parent' }),
],
}),
],
})Handle multi-touch gestures with two or more pointers.
let angle = 0
interact('#rotate-area').gesturable({
onmove: function (event) {
const arrow = document.getElementById('arrow')
angle += event.da
arrow.style.transform = 'rotate(' + angle + 'deg)'
document.getElementById('angle-info').textContent = angle.toFixed(2) + '°'
},
})<div id="rotate-area">
<div id="angle-info">0°</div>
<svg id="arrow" viewBox="0 0 100 100">
<polygon points="50,0 75,25 62.5,25 62.5,100 37.5,100 37.5,25 25,25" fill="#29e"></polygon>
</svg>
</div>#rotate-area {
touch-action: none;
user-select: none;
}| Property | Description |
|---|---|
| distance | Distance between the first two touches |
| angle | Angle of the line made by the two touches |
| da | Change in angle since previous event |
| scale | Ratio of start distance to current distance |
| ds | Change in scale since previous event |
| box | Box enclosing all touch points |
Define elements that draggable targets can be dropped into.
interact(dropTarget)
.dropzone({
ondrop: function (event) {
alert(event.relatedTarget.id + ' was dropped into ' + event.target.id)
}
})
.on('dropactivate', function (event) {
event.target.classList.add('drop-activated')
})| Event | Description |
|---|---|
| dropactivate | A draggable started that can be dropped here |
| dropdeactivate | A draggable ended |
| dragenter | A draggable entered the dropzone |
| dragleave | A draggable left the dropzone |
| dropmove | A draggable moved within the dropzone |
| drop | A draggable was dropped in the dropzone |
| Property | Description |
|---|---|
| target | The dropzone element |
| dropzone | The dropzone Interactable |
| relatedTarget | The element being dragged |
| draggable | The Interactable being dragged |
| dragEvent | The related drag event (dragstart, dragmove, dragend) |
| timeStamp | Time of the event |
| type | The event type |
Specify which draggable elements can be dropped:
interact('.dropzone').dropzone({
accept: '.drag0, .drag1',
})Controls how drops are checked:
interact(target).dropzone({
overlap: 0.25 // 'pointer' | 'center' | number (0-1)
})| Value | Description |
|---|---|
| 'pointer' | (default) Pointer must be over the dropzone |
| 'center' | Draggable element's center must be over the dropzone |
| 0-1 | (intersection area) / (draggable area) required for drop |
Custom drop validation:
interact(target).dropzone({
checker: function (
dragEvent, // related dragmove or dragend
event, // Touch, Pointer or Mouse Event
dropped, // bool default checker result
dropzone, // dropzone Interactable
dropzoneElement, // dropzone element
draggable, // draggable Interactable
draggableElement // draggable element
) {
// only allow drops into empty dropzone elements
return dropped && !dropzoneElement.hasChildNodes()
}
})| Action | Events |
|---|---|
| Draggable | dragstart, dragmove, draginertiastart, dragend |
| Resizable | resizestart, resizemove, resizeinertiastart, resizeend |
| Gesturable | gesturestart, gesturemove, gestureend |
function listener(event) {
event.target.textContent = `${event.type} at ${event.pageX}, ${event.pageY}`
}
interact(target)
.on('dragstart', listener)
.on('dragmove dragend', listener)
.on(['resizemove', 'resizeend'], listener)
.on({
gesturestart: listener,
gestureend: listener,
})
// Or using action options
interact(target).draggable({
onstart: listener,
onmove: listener,
onend: listener,
})
interact(target).resizable({
listeners: [
{
start: function (event) {
console.log(event.type, event.pageX, event.pageY)
},
},
],
})| Property | Description |
|---|---|
| target | The element being interacted with |
| interactable | The Interactable being interacted with |
| interaction | The Interaction the event belongs to |
| x0, y0 | Page x and y coordinates of the starting event |
| clientX0, clientY0 | Client x and y coordinates of the starting event |
| dx, dy | Change in coordinates of the mouse/touch |
| velocityX, velocityY | The velocity of the pointer |
| speed | The speed of the pointer |
| timeStamp | The time of creation of the event object |
interact(target).on('hold', function (event) {
console.log(event.type, event.target)
})Available pointer events:
down- Pointer pressed downmove- Pointer movedup- Pointer releasedcancel- Interaction cancelledtap- Quick press and releasedoubletap- Two taps in quick successionhold- Pointer held for ~600ms
interact(target).pointerEvents({
holdDuration: 1000,
ignoreFrom: '[no-pointer]',
allowFrom: '.handle',
origin: 'self',
})Use tap for fast clicks without mobile delay:
interact('a[href]').on('tap', function (event) {
window.location.href = event.currentTarget.href
event.preventDefault()
})Modifiers change the coordinates of action events. Apply them using the modifiers array in action options.
// Create modifiers
const restrictToParent = interact.modifiers.restrict({
restriction: 'parent',
elementRect: { left: 0, right: 0, top: 1, bottom: 1 },
})
const snap100x100 = interact.modifiers.snap({
targets: [interact.snappers.grid({ x: 100, y: 100 })],
relativePoints: [{ x: 0.5, y: 0.5 }],
})
// Apply to action
interact(target)
.draggable({
modifiers: [restrictToParent, snap100x100],
})
.on('dragmove', event => console.log(event.pageX, event.pageY))Note: Modifiers are applied sequentially; their order may affect the final result.
Apply modifier only to the last move event:
const snapAtEnd = interact.modifiers.snap({
endOnly: true,
targets: [/* ... */],
})Snap pointer coordinates to specified targets:
const mySnap = interact.modifiers.snap({
targets: [
{ x: 200, y: 200 },
{ x: 250, y: 350 },
],
})
interact(element).draggable({
modifiers: [mySnap]
})Specify points on the element for snapping:
interact(element).draggable({
modifiers: [
interact.modifiers.snap({
targets: [{ x: 300, y: 300 }],
relativePoints: [
{ x: 0, y: 0 }, // snap relative to top-left
{ x: 0.5, y: 0.5 }, // to the center
{ x: 1, y: 1 } // to the bottom-right
]
})
]
})Shift target coordinates:
interact(element).draggable({
modifiers: [
interact.modifiers.snap({
targets: [{ x: 300, y: 300 }],
offset: { x: 20, y: 20 } // or 'startCoords' | 'self' | 'parent'
})
]
})Snap dimensions when resizing:
interact(target).resizable({
edges: { top: true, left: true },
modifiers: [
interact.modifiers.snapSize({
targets: [
{ width: 100 },
interact.snappers.grid({ width: 100, height: 100 }),
],
}),
],
})Snap edges when resizing:
interact(target).resizable({
edges: { top: true, left: true },
modifiers: [
interact.modifiers.snapEdges({
targets: [
interact.snappers.grid({ top: 100, left: 100 }),
],
}),
],
})Create a grid snapping target:
const gridTarget = interact.snappers.grid({
x: 50, // horizontal grid spacing
y: 50, // vertical grid spacing
range: 10, // optional: snap range
offset: { x: 5, y: 10 }, // optional: offset grid lines
limits: { // optional: grid boundaries
top: 0, left: 0, bottom: 500, right: 500
}
})
interact(element).draggable({
modifiers: [interact.modifiers.snap({ targets: [gridTarget] })]
})interact.modifiers.snap({
targets: [
function (x, y, interaction, offset, index) {
return {
x: x,
y: 75 + 50 * Math.sin(x * 0.04),
range: 40,
}
},
],
})Restrict pointer coordinates to an area:
interact(target).draggable({
modifiers: [
interact.modifiers.restrict({
restriction: 'parent',
endOnly: true
})
]
})restriction values:
- Rect object:
{ top, left, bottom, right }or{ x, y, width, height } - Element: Use element's dimensions as restriction area
- Function:
(x, y, element) => rect or element - String:
'self','parent', or CSS selector
Restrict element edges (not just pointer):
interact(target).draggable({
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent'
})
]
})Specify which part of element to consider as its edges:
// Allow quarter of element to hang over restriction edges
interact(target).draggable({
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
elementRect: { top: 0.25, left: 0.25, bottom: 0.75, right: 0.75 }
})
]
})Set minimum and maximum dimensions when resizing:
interact(target).resizable({
modifiers: [
interact.modifiers.restrictSize({
min: { width: 100, height: 100 },
max: { width: 500, height: 500 }
})
]
})Set inner and outer edge boundaries when resizing:
interact(target).resizable({
modifiers: [
interact.modifiers.restrictEdges({
inner: {
left: 100, // left edge must be <= 100
right: 200 // right edge must be >= 200
},
outer: {
left: 0, // left edge must be >= 0
right: 300 // right edge must be <= 300
}
})
]
})Enable physics-based momentum after releasing elements:
interact(target)
.draggable({
inertia: true
})
.resizable({
inertia: {
resistance: 30,
minSpeed: 200,
endSpeed: 100
}
})| Option | Description |
|---|---|
| resistance | Rate at which action slows down (higher = slower) |
| endSpeed | Speed (px/s) at which action is considered stopped |
| allowResume | Allow user to resume action during inertia phase |
| smoothEndDuration | Duration (ms) of interpolated movement to endOnly snap/restrict coords |
Options available for draggable(), resizable(), and gesturable():
interact(target).draggable({
max: 3, // max concurrent interactions on this interactable
maxPerElement: 2 // max on same interactable+element combination (default: 1)
})Disable auto-start and require manual Interaction#start:
interact(target)
.draggable({
manualStart: true,
})
.on('doubletap', function (event) {
var interaction = event.interaction
if (!interaction.interacting()) {
interaction.start(
{ name: 'drag' },
event.interactable,
event.currentTarget,
)
}
})Start action after pointer held down for specified milliseconds:
interact(target).draggable({
hold: 500 // 500ms delay before drag starts
})Scroll container when dragging near edges:
interact(element)
.draggable({
autoScroll: true,
})
.resizable({
autoScroll: {
container: document.body,
margin: 50,
distance: 5,
interval: 10,
speed: 300,
}
})Require action to start from specific child element:
<div class="movable-box">
<div class="drag-handle"></div>
Content
<div class="resize-handle"></div>
</div>interact('.movable-box')
.draggable({
allowFrom: '.drag-handle',
})
.resizable({
allowFrom: '.resize-handle',
})Prevent action from starting on specific child elements:
<div id="movable-box">
<p class="content">Selectable text</p>
<div no-pointer-event>Should not fire events</div>
</div>interact('#movable-box')
.draggable({
ignoreFrom: '.content',
})
.pointerEvents({
ignoreFrom: '[no-pointer-event]',
})Disable automatic cursor styling:
interact(target).styleCursor(false)Custom cursor for each action:
interact(target).resizable({
edges: { left: true, right: true },
cursorChecker(action, interactable, element, interacting) {
if (action.edges.left) return 'w-resize'
if (action.edges.right) return 'e-resize'
},
})Enable or disable the action:
interact(target).draggable({
enabled: false // disable dragging
})<!DOCTYPE html>
<html>
<head>
<style>
.draggable {
width: 100px;
height: 100px;
background: #29e;
color: white;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
user-select: none;
cursor: move;
}
.dropzone {
width: 300px;
height: 300px;
border: 2px dashed #666;
display: flex;
align-items: center;
justify-content: center;
}
.dropzone.active {
border-color: #29e;
}
.dropzone.hover {
background: rgba(41, 158, 238, 0.1);
}
</style>
</head>
<body>
<div class="draggable" id="drag-1">Drag me</div>
<div class="dropzone" id="drop-1">Drop here</div>
<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<script>
const position = { x: 0, y: 0 }
interact('.draggable')
.draggable({
inertia: true,
modifiers: [
interact.modifiers.snap({
targets: [interact.snappers.grid({ x: 30, y: 30 })],
range: Infinity,
relativePoints: [{ x: 0.5, y: 0.5 }]
}),
interact.modifiers.restrict({
restriction: 'body',
elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
endOnly: true
})
],
autoScroll: true,
listeners: {
move(event) {
position.x += event.dx
position.y += event.dy
event.target.style.transform = `translate(${position.x}px, ${position.y}px)`
},
}
})
interact('.dropzone')
.dropzone({
accept: '.draggable',
overlap: 0.5,
ondropactivate(event) {
event.target.classList.add('active')
},
ondragenter(event) {
event.target.classList.add('hover')
},
ondragleave(event) {
event.target.classList.remove('hover')
},
ondrop(event) {
console.log(event.relatedTarget.id + ' dropped into ' + event.target.id)
},
ondropdeactivate(event) {
event.target.classList.remove('active', 'hover')
}
})
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<style>
.resizable {
width: 200px;
height: 200px;
background: #29e;
color: white;
padding: 20px;
box-sizing: border-box;
touch-action: none;
user-select: none;
}
</style>
</head>
<body>
<div class="resizable">Resize from any edge or corner</div>
<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<script>
interact('.resizable')
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move(event) {
const target = event.target
let x = (parseFloat(target.getAttribute('data-x')) || 0)
let y = (parseFloat(target.getAttribute('data-y')) || 0)
// update the element's style
target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
x += event.deltaRect.left
y += event.deltaRect.top
target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
target.textContent = Math.round(event.rect.width) + '×' + Math.round(event.rect.height)
}
},
modifiers: [
interact.modifiers.restrictEdges({
outer: 'parent'
}),
interact.modifiers.restrictSize({
min: { width: 100, height: 50 }
})
],
inertia: true
})
</script>
</body>
</html>InteractJS supports modern browsers including Chrome, Firefox, Safari, Edge, and mobile browsers. It handles both mouse and touch inputs.
MIT License - Copyright (c) 2012-present Taye Adeyemi