Introduction
Neodrag plugins, and how to author them.
Plugins are modular pieces that add specific dragging behaviors. Instead of one giant library, you mix and match only what you need.
Want grid snapping? Add grid()
. Need movement constraints? Use bounds()
. Want both? Use them together. This keeps bundles small and code clean.
Built-in Plugins
<script setup>
import {
vDraggable,
grid,
axis,
bounds,
BoundsFrom,
} from '@neodrag/vue';
const plugins = [
grid([20, 20]),
axis('x'),
bounds(BoundsFrom.viewport()),
];
</script>
<template>
<div v-draggable="plugins">Drag me around!</div>
</template>
Available plugins:
axis()
- Constrain movement to x or y axisbounds()
- Keep element within boundariesgrid()
- Snap to grid positionsposition()
- Set initial or forced positionsscrollLock()
- Prevent page scrolling while draggingevents()
- Handle drag lifecycle events
Dynamic Plugins with Compartments
For plugins that change based on user interaction, use Compartments:
<script setup>
import { ref } from 'vue';
import { vDraggable, axis, useCompartment } from '@neodrag/vue';
const currentAxis = ref('x');
const axisComp = useCompartment(() => axis(currentAxis.value));
</script>
<template>
<div v-draggable="() => [axisComp]">Drag me</div>
<button @click="currentAxis = 'y'">Switch to Y axis</button>
</template>
Important: v3 requires Compartments for dynamic behavior. Simply changing plugin options or recreating arrays wonβt update the draggable.
Writing Custom Plugins
Create a simple logging plugin:
const logger = {
name: 'my:logger',
start(ctx, state, event) {
console.log('Started dragging at:', ctx.initial.x, ctx.initial.y);
},
drag(ctx, state, event) {
console.log('Current position:', ctx.offset.x, ctx.offset.y);
},
end(ctx, state, event) {
console.log('Stopped dragging at:', ctx.offset.x, ctx.offset.y);
},
};
Use it like any built-in plugin:
const plugins = [logger, grid([10, 10])];
Configurable Plugins
Make plugins configurable with a factory function:
import { unstable_definePlugin } from '@neodrag/core/plugins';
const logger = unstable_definePlugin((prefix = 'DRAG') => ({
name: 'my:logger',
start(ctx) {
console.log(
`${prefix}: Started at`,
ctx.initial.x,
ctx.initial.y,
);
},
drag(ctx) {
console.log(`${prefix}: Moving to`, ctx.offset.x, ctx.offset.y);
},
end(ctx) {
console.log(`${prefix}: Ended at`, ctx.offset.x, ctx.offset.y);
},
}));
// Use with custom prefix
const plugins = [logger('MY_APP')];
Plugin Lifecycle
Plugins can implement these hooks that run at different drag stages:
interface Plugin {
name: string; // Unique identifier
priority?: number; // Higher = runs earlier (default: 0)
liveUpdate?: boolean; // Can update during active drag
cancelable?: boolean; // Respects cancellation (default: true)
setup?: (ctx) => state; // Initialize plugin
shouldStart?: (ctx, state, event) => boolean; // Should dragging begin?
start?: (ctx, state, event) => void; // Dragging started
drag?: (ctx, state, event) => void; // During dragging
end?: (ctx, state, event) => void; // Dragging ended
cleanup?: (ctx, state) => void; // Plugin destroyed
}
Execution flow:
setup
- Plugin initializes, returns state objectshouldStart
- User pressed down, should we start dragging?start
- Dragging beginsdrag
- Called repeatedly while draggingend
- User released, dragging stopscleanup
- Plugin destroyed (when instance is destroyed)
Plugin Context
The ctx
parameter provides access to drag state and actions:
interface PluginContext {
// Position data
offset: { x: number; y: number }; // Current position
initial: { x: number; y: number }; // Starting position
delta: { x: number; y: number }; // Change since last event
proposed: { x: number | null; y: number | null }; // Next position
// State flags
isDragging: boolean; // Currently in drag operation
isInteracting: boolean; // User is actively interacting
// DOM references
rootNode: HTMLElement | SVGElement; // The draggable element
currentlyDraggedNode: HTMLElement | SVGElement; // Element being dragged
lastEvent: PointerEvent | null; // Most recent pointer event
// Actions
propose(x: number | null, y: number | null): void; // Set next position
cancel(): void; // Cancel current drag
preventStart(): void; // Prevent drag from starting
setForcedPosition(x: number, y: number): void; // Jump to position
}
Plugin State
Plugins can maintain state between lifecycle calls:
const statefulPlugin = {
name: 'stateful:example',
setup(ctx) {
return {
startTime: null,
moveCount: 0,
};
},
start(ctx, state, event) {
state.startTime = Date.now();
state.moveCount = 0;
},
drag(ctx, state, event) {
state.moveCount++;
console.log(`Move #${state.moveCount}`);
},
end(ctx, state, event) {
const duration = Date.now() - state.startTime;
console.log(
`Dragged for ${duration}ms with ${state.moveCount} moves`,
);
},
};
Third-Party Plugins
Create publishable plugins for the community:
// my-awesome-plugin.ts
import { unstable_definePlugin } from '@neodrag/core/plugins';
export const awesomePlugin = unstable_definePlugin(
(options: AwesomePluginOptions = {}) => ({
name: 'awesome:plugin',
setup(ctx) {
return {
/* state */
};
},
// ... other hooks
}),
);
export interface AwesomePluginOptions {
speed?: number;
color?: string;
}
Publish as neodrag-plugin-awesome
:
npm install neodrag-plugin-awesome
import { awesomePlugin } from 'neodrag-plugin-awesome';
const plugins = [awesomePlugin({ speed: 2, color: 'blue' })];
Debugging
Add logging to understand plugin execution:
const debugPlugin = {
name: 'debug:execution',
priority: 999, // Run early
setup(ctx) {
console.log('π§ Plugin setup');
return {};
},
shouldStart(ctx, state, event) {
console.log('π€ Should start?', event.type);
return true;
},
start(ctx, state, event) {
console.log('π Drag started');
},
drag(ctx, state, event) {
console.log('π Dragging:', ctx.offset);
},
end(ctx, state, event) {
console.log('π Drag ended');
},
};
Inspect active instances:
// Check all active draggable instances
console.log('Active draggables:', instances.size);
// Inspect specific instance
for (const [element, instance] of instances) {
console.log('Element:', element);
console.log(
'Plugins:',
instance.plugins.map((p) => p.name),
);
console.log('Current state:', instance.ctx);
}