Skip to main content
Modifier is the base class for transforming drag operation coordinates. Modifiers enable features like snapping to grids, restricting movement to specific boundaries, applying physics-based constraints, and implementing custom drag behaviors.

Modifier Class

Constructor

Creates a new modifier instance.
class MyModifier extends Modifier<T, U> {
  constructor(manager: T, options?: U) {
    super(manager, options);
  }
}
manager
T extends DragDropManager
required
The drag and drop manager that owns this modifier.
options
U extends ModifierOptions
Optional configuration for the modifier.
T
extends DragDropManager<any, any>
default:"DragDropManager<any, any>"
Type parameter for the drag and drop manager.
U
extends ModifierOptions
default:"ModifierOptions"
Type parameter for modifier options.

Properties

manager

manager
T
The drag and drop manager instance that this modifier is bound to.
const manager = modifier.manager;

options

options
U | undefined
The configuration options for this modifier instance.
const options = modifier.options;

disabled

disabled
boolean
Whether the modifier instance is disabled.
modifier.disabled = true;
Inherited from Plugin.

Methods

apply()

Applies the modifier to the current drag operation.
public apply(operation: DragOperationSnapshot): Coordinates;
operation
DragOperationSnapshot<any, any>
required
The current state of the drag operation.
return
Coordinates
The transformed coordinates.
{x: number, y: number}
The default implementation returns the original transform unchanged. Override this method to implement custom transformation logic.

Inherited from Plugin

enable()

Enables a disabled modifier instance.
modifier.enable();

disable()

Disables an enabled modifier instance.
modifier.disable();

isDisabled()

Checks if the modifier instance is disabled.
const disabled = modifier.isDisabled();
return
boolean
true if the modifier is disabled.

configure()

Configures a modifier instance with new options.
modifier.configure({option: 'value'});
options
U
The new options to apply.

destroy()

Destroys a modifier instance and cleans up its resources.
modifier.destroy();

Static Methods

configure()

Configures a modifier constructor with default options.
const ConfiguredModifier = MyModifier.configure({
  gridSize: 20,
  boundary: 'window'
});
options
PluginOptions
required
The options to configure the constructor with.
return
ModifierDescriptor
A configured modifier descriptor that can be passed to the manager or draggable.

Types

ModifierOptions

Base type for modifier options.
type ModifierOptions = PluginOptions;
type PluginOptions = Record<string, any>;

ModifierConstructor

Constructor type for creating modifier instances.
type ModifierConstructor<T extends DragDropManager<any, any>> = 
  PluginConstructor<T, Modifier<T, any>>;

interface PluginConstructor<T, U> {
  new (manager: T, options?: PluginOptions): U;
}

ModifierDescriptor

Descriptor type for configuring modifiers.
type ModifierDescriptor<T extends DragDropManager<any, any>> = 
  PluginDescriptor<T, Modifier<T, any>, ModifierConstructor<T>>;

type PluginDescriptor<T, U, V> = {
  plugin: V;
  options?: PluginOptions;
};

Modifiers

Array type for multiple modifier configurations.
type Modifiers<T extends DragDropManager<any, any>> = 
  (ModifierConstructor<T> | ModifierDescriptor<T>)[];

Implementing a Custom Modifier

Basic Implementation

import {Modifier, type ModifierOptions} from '@dnd-kit/abstract';
import type {DragOperationSnapshot, Coordinates} from '@dnd-kit/abstract';

interface SnapModifierOptions extends ModifierOptions {
  gridSize: number;
}

class SnapModifier extends Modifier<DragDropManager, SnapModifierOptions> {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform} = operation;
    const gridSize = this.options?.gridSize ?? 20;
    
    return {
      x: Math.round(transform.x / gridSize) * gridSize,
      y: Math.round(transform.y / gridSize) * gridSize
    };
  }
}

Advanced Implementation

import {Modifier, type ModifierOptions} from '@dnd-kit/abstract';
import type {Coordinates, Shape} from '@dnd-kit/geometry';

interface RestrictModifierOptions extends ModifierOptions {
  boundary: 'window' | 'parent' | Shape;
}

class RestrictModifier extends Modifier<DragDropManager, RestrictModifierOptions> {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform, shape} = operation;
    const boundary = this.options?.boundary;
    
    if (!boundary || !shape) {
      return transform;
    }
    
    const bounds = this.getBounds(boundary);
    const draggableRect = shape.current;
    
    // Calculate restricted position
    let x = transform.x;
    let y = transform.y;
    
    if (draggableRect.x + x < bounds.left) {
      x = bounds.left - draggableRect.x;
    }
    if (draggableRect.x + x + draggableRect.width > bounds.right) {
      x = bounds.right - draggableRect.x - draggableRect.width;
    }
    if (draggableRect.y + y < bounds.top) {
      y = bounds.top - draggableRect.y;
    }
    if (draggableRect.y + y + draggableRect.height > bounds.bottom) {
      y = bounds.bottom - draggableRect.y - draggableRect.height;
    }
    
    return {x, y};
  }
  
  private getBounds(boundary: RestrictModifierOptions['boundary']) {
    if (boundary === 'window') {
      return {
        left: 0,
        top: 0,
        right: window.innerWidth,
        bottom: window.innerHeight
      };
    }
    // Handle other boundary types...
  }
}

Chaining Modifiers

Modifiers are applied in sequence, with each modifier receiving the output of the previous one:
class LoggingModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    console.log('Before:', operation.transform);
    
    // Pass through unchanged
    return operation.transform;
  }
}

const manager = new DragDropManager({
  modifiers: [
    SnapModifier.configure({gridSize: 20}),
    RestrictModifier.configure({boundary: 'window'}),
    LoggingModifier
  ]
});
// Execution order: Snap -> Restrict -> Logging

Usage Examples

Using Modifiers with Manager

import {DragDropManager} from '@dnd-kit/abstract';
import {SnapModifier, RestrictModifier} from '@dnd-kit/abstract/modifiers';

const manager = new DragDropManager({
  modifiers: [
    SnapModifier.configure({gridSize: 20}),
    RestrictModifier.configure({boundary: 'window'})
  ]
});

Per-Draggable Modifiers

import {Draggable} from '@dnd-kit/abstract';
import {SnapModifier} from '@dnd-kit/abstract/modifiers';

const draggable = new Draggable(
  {
    id: 'item-1',
    modifiers: [
      SnapModifier.configure({gridSize: 10})
    ]
  },
  manager
);
// This draggable uses its own modifiers, overriding the manager's

Dynamic Modifier Configuration

const manager = new DragDropManager();

// Add modifiers later
manager.modifiers = [
  SnapModifier.configure({gridSize: 20})
];

// Update modifiers
manager.modifiers = [
  SnapModifier.configure({gridSize: 40}),
  RestrictModifier.configure({boundary: 'parent'})
];

Conditional Modification

class ConditionalModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform, source} = operation;
    
    // Only apply to certain draggables
    if (source?.data.snapToGrid) {
      const gridSize = 20;
      return {
        x: Math.round(transform.x / gridSize) * gridSize,
        y: Math.round(transform.y / gridSize) * gridSize
      };
    }
    
    return transform;
  }
}

Accessing Operation State

class SmartModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {
      transform,
      source,
      target,
      position,
      shape,
      activatorEvent
    } = operation;
    
    // Use source data
    if (source?.data.restricted) {
      // Apply restrictions
    }
    
    // Check if over a target
    if (target) {
      // Snap to target center
      const targetShape = target.shape;
      if (targetShape && shape) {
        return {
          x: targetShape.center.x - shape.initial.center.x,
          y: targetShape.center.y - shape.initial.center.y
        };
      }
    }
    
    // Check movement delta
    const delta = position.delta;
    if (Math.abs(delta.x) < 5 && Math.abs(delta.y) < 5) {
      // Minimal movement, return to origin
      return {x: 0, y: 0};
    }
    
    return transform;
  }
}

Disable/Enable Modifiers

const snapModifier = new SnapModifier(manager, {gridSize: 20});

// Temporarily disable
snapModifier.disable();

// Re-enable
snapModifier.enable();

// Check status
if (snapModifier.isDisabled()) {
  console.log('Snapping is disabled');
}

Common Modifier Patterns

Grid Snapping

class GridSnapModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform} = operation;
    const gridSize = this.options?.gridSize ?? 20;
    
    return {
      x: Math.round(transform.x / gridSize) * gridSize,
      y: Math.round(transform.y / gridSize) * gridSize
    };
  }
}

Restrict to Container

class RestrictToContainerModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform, shape} = operation;
    if (!shape) return transform;
    
    const container = this.options?.container;
    if (!container) return transform;
    
    const bounds = container.getBoundingClientRect();
    const rect = shape.current;
    
    return {
      x: Math.max(
        bounds.left - rect.x,
        Math.min(transform.x, bounds.right - rect.x - rect.width)
      ),
      y: Math.max(
        bounds.top - rect.y,
        Math.min(transform.y, bounds.bottom - rect.y - rect.height)
      )
    };
  }
}

Axis Lock

class AxisLockModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform} = operation;
    const axis = this.options?.axis; // 'x' | 'y'
    
    if (axis === 'x') {
      return {x: transform.x, y: 0};
    }
    if (axis === 'y') {
      return {x: 0, y: transform.y};
    }
    
    return transform;
  }
}

Resistance

class ResistanceModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform} = operation;
    const factor = this.options?.factor ?? 0.5;
    
    return {
      x: transform.x * factor,
      y: transform.y * factor
    };
  }
}

Snap to Elements

class SnapToElementsModifier extends Modifier {
  public apply(operation: DragOperationSnapshot): Coordinates {
    const {transform, shape} = operation;
    if (!shape) return transform;
    
    const snapTargets = this.options?.targets ?? [];
    const threshold = this.options?.threshold ?? 20;
    
    const currentCenter = {
      x: shape.initial.center.x + transform.x,
      y: shape.initial.center.y + transform.y
    };
    
    for (const target of snapTargets) {
      const targetRect = target.getBoundingClientRect();
      const targetCenter = {
        x: targetRect.left + targetRect.width / 2,
        y: targetRect.top + targetRect.height / 2
      };
      
      const distance = Math.hypot(
        currentCenter.x - targetCenter.x,
        currentCenter.y - targetCenter.y
      );
      
      if (distance < threshold) {
        return {
          x: targetCenter.x - shape.initial.center.x,
          y: targetCenter.y - shape.initial.center.y
        };
      }
    }
    
    return transform;
  }
}

See Also

Build docs developers (and LLMs) love