Modifiers transform the coordinates of a drag operation, enabling features like axis constraints, snap-to-grid, boundary restrictions, and custom transformations.
Overview
Modifiers:
Transform drag coordinates before rendering
Run in a pipeline (each modifier receives the output of the previous)
Can be configured globally or per-draggable
Support options for customization
Are reactive and can be enabled/disabled dynamically
How modifiers work
During a drag operation:
User drags an element
Sensor updates position
Position delta is calculated: {x: currentX - initialX, y: currentY - initialY}
Each modifier transforms the coordinates in sequence
Final transform is applied for rendering
// From dragOperation.ts
get transform () {
const { x , y } = this . position . delta ;
let transform = { x , y };
for ( const modifier of this . modifiers ) {
transform = modifier . apply ({
... this . snapshot (),
transform ,
});
}
return transform ;
}
Modifier base class
All modifiers extend the Modifier base class:
class Modifier <
T extends DragDropManager < any , any > = DragDropManager < any , any >,
U extends ModifierOptions = ModifierOptions
> extends Plugin < T , U > {
constructor (
public manager : T ,
public options ?: U
);
apply ( operation : DragOperationSnapshot ) : Coordinates {
return operation . transform ;
}
}
Override the apply method to transform coordinates:
class MyModifier extends Modifier {
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform } = operation ;
// Transform coordinates
return {
x: /* modified x */ ,
y: /* modified y */ ,
};
}
}
Built-in modifiers
Axis constraints
Restrict movement to a single axis:
Vertical axis:
import { RestrictToVerticalAxis } from '@dnd-kit/abstract' ;
const manager = new DragDropManager ({
modifiers: [ RestrictToVerticalAxis ],
});
Fixes x-coordinate to 0, allowing only vertical movement.
Horizontal axis:
import { RestrictToHorizontalAxis } from '@dnd-kit/abstract' ;
const manager = new DragDropManager ({
modifiers: [ RestrictToHorizontalAxis ],
});
Fixes y-coordinate to 0, allowing only horizontal movement.
Implementation:
// From abstract/src/modifiers/axis.ts
class AxisModifier extends Modifier < DragDropManager , Options > {
apply ({ transform } : DragOperation ) {
if ( ! this . options ) return transform ;
const { axis , value } = this . options ;
return {
... transform ,
[axis]: value ,
};
}
}
export const RestrictToVerticalAxis = AxisModifier . configure ({
axis: 'x' ,
value: 0 ,
});
export const RestrictToHorizontalAxis = AxisModifier . configure ({
axis: 'y' ,
value: 0 ,
});
Snap to grid
Snap coordinates to a grid:
import { SnapToGrid } from '@dnd-kit/abstract' ;
const manager = new DragDropManager ({
modifiers: [
SnapToGrid . configure ({ gridSize: 20 }),
],
});
Implementation:
// From abstract/src/modifiers/snap.ts
interface Options {
gridSize : number ;
}
class SnapModifier extends Modifier < DragDropManager , Options > {
apply ({ transform } : DragOperation ) : Coordinates {
const { gridSize = 1 } = this . options ?? {};
return {
x: Math . round ( transform . x / gridSize ) * gridSize ,
y: Math . round ( transform . y / gridSize ) * gridSize ,
};
}
}
export const SnapToGrid = SnapModifier . configure ;
Bounding rectangle
Constrain movement within a rectangle:
import { BoundingRectangle } from '@dnd-kit/abstract' ;
const manager = new DragDropManager ({
modifiers: [
BoundingRectangle . configure ({
bounds: { x: 0 , y: 0 , width: 800 , height: 600 },
}),
],
});
Implementation:
// From abstract/src/modifiers/boundingRectangle.ts
interface Options {
bounds : Rectangle ;
}
class BoundingRectangleModifier extends Modifier < DragDropManager , Options > {
apply ( operation : DragOperation ) : Coordinates {
const { transform , shape } = operation ;
const { bounds } = this . options ?? {};
if ( ! bounds || ! shape ) return transform ;
const { current } = shape ;
return {
x: clamp ( transform . x , bounds . left - current . left , bounds . right - current . right ),
y: clamp ( transform . y , bounds . top - current . top , bounds . bottom - current . bottom ),
};
}
}
DOM-specific modifiers
The @dnd-kit/dom package provides DOM-specific modifiers:
Restrict to window:
import { RestrictToWindow } from '@dnd-kit/dom/modifiers' ;
const manager = new DragDropManager ({
modifiers: [ RestrictToWindow ],
});
Prevents dragged elements from leaving the viewport.
Restrict to element:
import { RestrictToElement } from '@dnd-kit/dom/modifiers' ;
const manager = new DragDropManager ({
modifiers: [
RestrictToElement . configure ({
element: document . getElementById ( 'container' ),
}),
],
});
Constrains movement within a specific DOM element.
Configuration
Global modifiers
Apply to all draggables:
const manager = new DragDropManager ({
modifiers: [
RestrictToVerticalAxis ,
SnapToGrid . configure ({ gridSize: 20 }),
],
});
Per-draggable modifiers
Override for specific draggables:
import { RestrictToHorizontalAxis } from '@dnd-kit/abstract' ;
const draggable = new Draggable ({
id: 'item-1' ,
element: myElement ,
// Only this draggable uses horizontal constraint
modifiers: [ RestrictToHorizontalAxis ],
});
Per-draggable modifiers replace the manager’s modifiers for that draggable.
Dynamic modifiers
You can change modifiers during runtime:
// Update manager modifiers
manager . modifiers = [ NewModifier ];
// Update per-draggable modifiers
draggable . modifiers = [ DifferentModifier ];
The drag operation automatically updates to use the new modifiers.
Modifier pipeline
Modifiers run in sequence, each receiving the output of the previous:
const manager = new DragDropManager ({
modifiers: [
SnapToGrid . configure ({ gridSize: 20 }), // First: snap to grid
RestrictToWindow , // Then: constrain to window
],
});
Order matters:
// Snap, then constrain
[ SnapToGrid , RestrictToWindow ]
// Result: Snapped coordinates may be outside window, then clamped
// Constrain, then snap
[ RestrictToWindow , SnapToGrid ]
// Result: Constrained coordinates, then snapped (may go outside again)
Put general transforms (snap, scale) before boundary constraints (restrict to window/element) for best results.
Custom modifiers
Create custom modifiers for specific behaviors:
Simple example
import { Modifier } from '@dnd-kit/abstract' ;
import type { DragDropManager , DragOperationSnapshot } from '@dnd-kit/abstract' ;
import type { Coordinates } from '@dnd-kit/geometry' ;
class ScaleModifier extends Modifier < DragDropManager , { scale : number }> {
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform } = operation ;
const { scale = 1 } = this . options ?? {};
return {
x: transform . x * scale ,
y: transform . y * scale ,
};
}
}
// Use it
const manager = new DragDropManager ({
modifiers: [
ScaleModifier . configure ({ scale: 0.5 }), // Half speed
],
});
Momentum modifier
class MomentumModifier extends Modifier {
#velocity = { x: 0 , y: 0 };
#lastTransform = { x: 0 , y: 0 };
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform } = operation ;
// Calculate velocity
this . #velocity . x = transform . x - this . #lastTransform . x ;
this . #velocity . y = transform . y - this . #lastTransform . y ;
this . #lastTransform = transform ;
// Apply momentum
return {
x: transform . x + this . #velocity . x * 0.1 ,
y: transform . y + this . #velocity . y * 0.1 ,
};
}
destroy () {
this . #velocity = { x: 0 , y: 0 };
this . #lastTransform = { x: 0 , y: 0 };
}
}
Conditional modifier
class ConditionalModifier extends Modifier < DragDropManager , { enabled : boolean }> {
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform } = operation ;
const { enabled = true } = this . options ?? {};
if ( ! enabled ) return transform ;
// Apply transformation only when enabled
return {
x: Math . round ( transform . x / 10 ) * 10 ,
y: Math . round ( transform . y / 10 ) * 10 ,
};
}
}
// Toggle at runtime
const modifier = new ConditionalModifier ( manager , { enabled: false });
manager . modifiers = [ modifier ];
// Enable later
modifier . configure ({ enabled: true });
Operation snapshot
Modifiers receive a snapshot of the current drag operation:
interface DragOperationSnapshot {
activatorEvent : Event | null ; // Event that started the drag
canceled : boolean ; // Whether drag was canceled
position : Position ; // Current and initial position
transform : Coordinates ; // Current transform (from previous modifier)
status : Status ; // Drag status
shape : WithHistory < Shape > | null ; // Draggable shape (current, initial, previous)
source : Draggable | null ; // Draggable being dragged
target : Droppable | null ; // Current drop target
}
Use this data to make informed decisions:
class SmartModifier extends Modifier {
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform , target , shape } = operation ;
// Different behavior based on target
if ( target ) {
// Over a droppable: snap to grid
return {
x: Math . round ( transform . x / 20 ) * 20 ,
y: Math . round ( transform . y / 20 ) * 20 ,
};
}
// Not over a droppable: free movement
return transform ;
}
}
Lifecycle
Modifiers follow the plugin lifecycle:
class MyModifier extends Modifier {
constructor ( manager : DragDropManager , options ?: ModifierOptions ) {
super ( manager , options );
// Set up effects
this . registerEffect (() => {
// React to drag operation changes
});
}
apply ( operation : DragOperationSnapshot ) : Coordinates {
// Transform coordinates
return operation . transform ;
}
destroy () {
// Clean up resources
super . destroy ();
}
}
Enable/disable
Modifiers can be enabled or disabled:
const modifier = new MyModifier ( manager );
// Disable
modifier . disable ();
// Enable
modifier . enable ();
// Check status
if ( modifier . isDisabled ()) {
// Modifier is disabled
}
Disabled modifiers still run but can check their status:
class MyModifier extends Modifier {
apply ( operation : DragOperationSnapshot ) : Coordinates {
if ( this . isDisabled ()) {
return operation . transform ; // Pass through
}
// Apply transformation
return { /* ... */ };
}
}
Combining modifiers
You can create complex behaviors by combining modifiers:
import {
RestrictToVerticalAxis ,
SnapToGrid ,
BoundingRectangle ,
} from '@dnd-kit/abstract' ;
const manager = new DragDropManager ({
modifiers: [
RestrictToVerticalAxis , // Only vertical movement
SnapToGrid . configure ({ // Snap to 20px grid
gridSize: 20 ,
}),
BoundingRectangle . configure ({ // Stay within bounds
bounds: { x: 0 , y: 0 , width: 400 , height: 800 },
}),
],
});
Result: Vertical-only movement, snapped to 20px grid, constrained within bounds.
Modifiers run on every position update during a drag. Keep them lightweight and avoid heavy computations.
Use memoization if your modifier performs expensive calculations: class ExpensiveModifier extends Modifier {
#cache = new Map ();
apply ( operation : DragOperationSnapshot ) : Coordinates {
const { transform } = operation ;
const key = ` ${ transform . x } , ${ transform . y } ` ;
if ( this . #cache . has ( key )) {
return this . #cache . get ( key ) ! ;
}
const result = /* expensive calculation */ ;
this . #cache . set ( key , result );
return result ;
}
}
Best practices
Order matters : Place general transforms before constraints
Keep it simple : Modifiers run frequently, avoid complex logic
Use options : Make modifiers configurable with options
Clean up : Implement destroy() if you allocate resources
Test combinations : Ensure modifiers work well together
Modifiers API API reference
DOM modifiers DOM-specific modifiers
Geometry Coordinate utilities