Plugins extend the core drag and drop functionality by adding features like visual feedback, auto-scrolling, accessibility announcements, and custom behaviors.
Overview
Plugins:
Extend drag and drop behavior without modifying core code
React to drag operation changes through effects
Can be configured globally or per-draggable
Support options for customization
Can be enabled/disabled dynamically
Follow a consistent lifecycle
Plugin base class
All plugins extend the Plugin base class:
abstract class Plugin <
T extends DragDropManager < any , any > = DragDropManager < any , any >,
U extends PluginOptions = PluginOptions
> {
constructor (
public manager : T ,
public options ?: U
);
disabled : boolean ;
enable () : void ;
disable () : void ;
isDisabled () : boolean ;
configure ( options ?: U ) : void ;
protected registerEffect ( callback : () => void ) : () => void ;
destroy () : void ;
static configure ( options : PluginOptions ) : PluginDescriptor ;
}
Built-in plugins
The @dnd-kit/dom package includes several useful plugins:
Feedback
Provides visual feedback during drag operations:
import { Feedback } from '@dnd-kit/dom' ;
const manager = new DragDropManager ({
plugins: [ Feedback ],
});
The feedback plugin:
Creates a visual representation of the dragged element
Follows the pointer during drag
Supports custom rendering
Handles animations and transitions
Configuration:
Feedback . configure ({
duration: 300 , // Animation duration
disabled: false , // Enable/disable
})
Accessibility
Provides screen reader announcements:
import { Accessibility } from '@dnd-kit/dom' ;
const manager = new DragDropManager ({
plugins: [ Accessibility ],
});
Announces:
When a drag starts
When hovering over a droppable
When a drag ends (dropped or canceled)
Automatically scrolls containers when dragging near edges:
import { AutoScroller } from '@dnd-kit/dom' ;
const manager = new DragDropManager ({
plugins: [ AutoScroller ],
});
Configuration:
AutoScroller . configure ({
threshold: 50 , // Distance from edge to trigger
speed: 10 , // Scroll speed
})
Cursor
Manages cursor styles during drag:
import { Cursor } from '@dnd-kit/dom' ;
const manager = new DragDropManager ({
plugins: [ Cursor ],
});
Sets appropriate cursors:
grabbing while dragging
grab on hover
not-allowed when can’t drop
PreventSelection
Prevents text selection during drag:
import { PreventSelection } from '@dnd-kit/dom' ;
const manager = new DragDropManager ({
plugins: [ PreventSelection ],
});
This plugin is included by default and prevents unwanted text selection.
Core plugins
Some plugins are required for core functionality:
CollisionNotifier
Updates the drag target based on collision detection:
// From collision/notifier.ts
class CollisionNotifier extends CorePlugin {
constructor ( manager : DragDropManager ) {
super ( manager );
this . registerEffect (() => {
const collisions = this . manager . collisionObserver . collisions ;
const collision = collisions [ 0 ]; // Highest priority
this . manager . dragOperation . targetIdentifier =
collision ?. id ?? null ;
});
}
}
This plugin is automatically included in every manager.
These DOM-specific plugins track and manage scroll offsets:
// Automatically included in @dnd-kit/dom
plugins : [ ScrollListener , Scroller , StyleInjector , ... userPlugins ]
Creating custom plugins
Extend the Plugin class to create custom functionality:
Basic example
import { Plugin } from '@dnd-kit/abstract' ;
import type { DragDropManager } from '@dnd-kit/abstract' ;
class LoggerPlugin extends Plugin < DragDropManager > {
constructor ( manager : DragDropManager ) {
super ( manager );
// React to drag operation changes
this . registerEffect (() => {
const { status } = this . manager . dragOperation ;
if ( status . dragging ) {
console . log ( 'Drag started' );
}
if ( status . idle ) {
console . log ( 'Drag ended' );
}
});
}
}
// Use it
const manager = new DragDropManager ({
plugins: [ LoggerPlugin ],
});
With options
interface LoggerOptions {
prefix ?: string ;
verbose ?: boolean ;
}
class LoggerPlugin extends Plugin < DragDropManager , LoggerOptions > {
constructor ( manager : DragDropManager , options ?: LoggerOptions ) {
super ( manager , options );
const { prefix = '[DnD]' , verbose = false } = options ?? {};
this . registerEffect (() => {
const { status , source , target } = this . manager . dragOperation ;
if ( status . dragging ) {
console . log ( ` ${ prefix } Started dragging:` , source ?. id );
if ( verbose ) {
console . log ( ` ${ prefix } Source data:` , source ?. data );
}
}
if ( status . dropping && target ) {
console . log ( ` ${ prefix } Dropping on:` , target . id );
}
});
}
}
// Configure and use
const manager = new DragDropManager ({
plugins: [
LoggerPlugin . configure ({
prefix: '[MyApp]' ,
verbose: true ,
}),
],
});
Analytics plugin
interface AnalyticsOptions {
trackingId : string ;
}
class AnalyticsPlugin extends Plugin < DragDropManager , AnalyticsOptions > {
#startTime : number | null = null ;
constructor ( manager : DragDropManager , options ?: AnalyticsOptions ) {
super ( manager , options );
this . registerEffect (() => {
const { status , source , target } = this . manager . dragOperation ;
if ( status . dragging && ! this . #startTime ) {
this . #startTime = Date . now ();
this . track ( 'drag_start' , {
source_id: source ?. id ,
source_type: source ?. type ,
});
}
if ( status . idle && this . #startTime ) {
const duration = Date . now () - this . #startTime ;
this . track ( 'drag_end' , {
source_id: source ?. id ,
target_id: target ?. id ,
duration_ms: duration ,
canceled: this . manager . dragOperation . canceled ,
});
this . #startTime = null ;
}
});
}
private track ( event : string , data : Record < string , any >) {
const { trackingId } = this . options ?? {};
// Send to analytics service
console . log ( 'Analytics:' , event , { trackingId , ... data });
}
destroy () {
this . #startTime = null ;
super . destroy ();
}
}
Visual feedback plugin
class HighlightPlugin extends Plugin < DragDropManager > {
#cleanup : (() => void ) | null = null ;
constructor ( manager : DragDropManager ) {
super ( manager );
this . registerEffect (() => {
const { target } = this . manager . dragOperation ;
// Clean up previous highlight
this . #cleanup ?.();
this . #cleanup = null ;
if ( target ?. element ) {
// Add highlight class
target . element . classList . add ( 'drop-target' );
this . #cleanup = () => {
target . element ?. classList . remove ( 'drop-target' );
};
}
});
}
destroy () {
this . #cleanup ?.();
super . destroy ();
}
}
Plugin lifecycle
Construction
Plugins are instantiated when registered with the manager:
// Manager creates plugin instances
const plugin = new MyPlugin ( manager , options );
Effects
Use registerEffect to react to drag operation changes:
class MyPlugin extends Plugin {
constructor ( manager : DragDropManager ) {
super ( manager );
// This effect runs whenever reactive values change
this . registerEffect (() => {
const { status } = this . manager . dragOperation ;
// React to status changes
console . log ( 'Status:' , status );
});
// Multiple effects are allowed
this . registerEffect (() => {
const { target } = this . manager . dragOperation ;
// React to target changes
console . log ( 'Target:' , target ?. id );
});
}
}
Destruction
Plugins are destroyed when the manager is destroyed:
class MyPlugin extends Plugin {
#intervalId : number | null = null ;
constructor ( manager : DragDropManager ) {
super ( manager );
this . #intervalId = setInterval (() => {
console . log ( 'Tick' );
}, 1000 );
}
destroy () {
// Clean up resources
if ( this . #intervalId !== null ) {
clearInterval ( this . #intervalId );
this . #intervalId = null ;
}
// Call parent destroy (cleans up effects)
super . destroy ();
}
}
Configuration
Global plugins
Apply to all drag operations:
const manager = new DragDropManager ({
plugins: [
Feedback ,
Accessibility ,
MyCustomPlugin . configure ({ /* options */ }),
],
});
Per-draggable plugins
Apply to specific draggables:
const draggable = new Draggable ({
id: 'item-1' ,
element: myElement ,
plugins: [
MyPlugin . configure ({ /* options */ }),
],
});
Per-draggable plugins are registered with the manager when the draggable is registered.
Plugin descriptors
The configure method returns a descriptor:
type PluginDescriptor = {
plugin : PluginConstructor ;
options : PluginOptions ;
};
// Creating a descriptor
const descriptor = MyPlugin . configure ({ option: 'value' });
// Descriptor contains:
// {
// plugin: MyPlugin,
// options: {option: 'value'}
// }
Managers and entities accept both constructors and descriptors:
// Constructor (uses default options)
plugins : [ MyPlugin ]
// Descriptor (uses custom options)
plugins : [ MyPlugin . configure ({ option: 'value' })]
Enable/disable plugins
Plugins can be enabled or disabled at runtime:
const plugin = new MyPlugin ( manager );
// Disable
plugin . disable ();
// Enable
plugin . enable ();
// Check
if ( plugin . isDisabled ()) {
// Plugin is disabled
}
Disabled plugins:
Still exist in the registry
Effects still run
Should check isDisabled() and skip their logic
class MyPlugin extends Plugin {
constructor ( manager : DragDropManager ) {
super ( manager );
this . registerEffect (() => {
if ( this . isDisabled ()) return ;
// Only run when enabled
const { status } = this . manager . dragOperation ;
console . log ( 'Status:' , status );
});
}
}
Accessing plugins
Get registered plugins from the manager:
const { plugins } = manager ;
// Find a specific plugin
const feedbackPlugin = plugins . find (
( p ) => p instanceof Feedback
);
// Configure at runtime
if ( feedbackPlugin ) {
feedbackPlugin . configure ({ duration: 500 });
}
Plugin registry
The manager maintains a plugin registry:
manager . registry . plugins . values // Get all plugin instances
manager . plugins // Shorthand for registry.plugins.values
Set plugins:
manager . plugins = [ NewPlugin ];
Per-draggable plugin configuration
Draggables can look up their plugin configuration:
class MyPlugin extends Plugin {
constructor ( manager : DragDropManager ) {
super ( manager );
this . registerEffect (() => {
const { source } = this . manager . dragOperation ;
if ( source ) {
// Get per-draggable options for this plugin
const options = source . pluginConfig ( MyPlugin );
if ( options ) {
console . log ( 'Per-draggable options:' , options );
}
}
});
}
}
Usage:
const draggable = new Draggable ({
id: 'item-1' ,
element: myElement ,
plugins: [
MyPlugin . configure ({ customOption: 'value' }),
],
});
// Later, in the plugin:
const options = draggable . pluginConfig ( MyPlugin );
// Returns: {customOption: 'value'}
Type safety
Plugins can be typed with their manager type:
class MyPlugin < T extends DragDropManager < any , any >> extends Plugin < T > {
constructor ( manager : T , options ?: MyOptions ) {
super ( manager , options );
// manager is typed as T
this . registerEffect (() => {
// Type-safe access
});
}
}
Infer types from manager:
import type { InferDraggable , InferDroppable } from '@dnd-kit/abstract' ;
class MyPlugin < T extends DragDropManager < any , any >> extends Plugin < T > {
constructor ( manager : T ) {
super ( manager );
this . registerEffect (() => {
const { source } = this . manager . dragOperation ;
// Type is inferred from manager
type Draggable = InferDraggable < T >;
const typedSource : Draggable | null = source ;
});
}
}
Best practices
Use effects for reactions : Always use registerEffect to react to drag operation changes. This ensures your plugin stays in sync with the reactive system.
Clean up resources : Implement destroy() if your plugin allocates resources (timers, event listeners, DOM elements).
Support options : Make your plugins configurable with options. Use the configure pattern for user-friendly configuration.
Respect disabled state : Check isDisabled() in your effects and skip logic when disabled.
Example: Undo/Redo plugin
interface UndoRedoOptions {
maxHistory ?: number ;
}
class UndoRedoPlugin extends Plugin < DragDropManager , UndoRedoOptions > {
#history : Array <{ source : string , target : string }> = [];
#index = - 1 ;
constructor ( manager : DragDropManager , options ?: UndoRedoOptions ) {
super ( manager , options );
this . registerEffect (() => {
const { status , source , target } = this . manager . dragOperation ;
if ( status . idle && source && target && ! this . manager . dragOperation . canceled ) {
// Record successful drop
const { maxHistory = 50 } = this . options ?? {};
// Remove any future history
this . #history = this . #history . slice ( 0 , this . #index + 1 );
// Add new entry
this . #history . push ({
source: source . id . toString (),
target: target . id . toString (),
});
// Limit history size
if ( this . #history . length > maxHistory ) {
this . #history . shift ();
} else {
this . #index ++ ;
}
}
});
}
undo () {
if ( this . #index < 0 ) return null ;
const action = this . #history [ this . #index ];
this . #index -- ;
return action ;
}
redo () {
if ( this . #index >= this . #history . length - 1 ) return null ;
this . #index ++ ;
const action = this . #history [ this . #index ];
return action ;
}
canUndo () {
return this . #index >= 0 ;
}
canRedo () {
return this . #index < this . #history . length - 1 ;
}
}
Custom plugins Build your own plugins
Modifiers Transform coordinates
Plugins API Plugin API reference
DOM plugins Built-in DOM plugins