position
Control element position programmatically
The position
plugin controls where draggable elements are positioned. Set initial positions, move elements programmatically, or create two-way binding between app state and element position.
position({ current: { x: 100, y: 50 } }); // Force to position
position({ default: { x: 0, y: 0 } }); // Set initial position
Basic Usage
import { useRef, useState } from 'react';
import {
position,
useDraggable,
useCompartment,
} from '@neodrag/react';
function PositionExample() {
const controlledRef = useRef<HTMLDivElement>(null);
const initialRef = useRef<HTMLDivElement>(null);
const [currentPos, setCurrentPos] = useState({ x: 100, y: 50 });
// Reactive compartments for changing values
const currentPosComp = useCompartment(
() => position({ current: currentPos }),
[currentPos],
);
// Static position can be used directly
const defaultPosComp = useCompartment(
() => position({ default: { x: 0, y: 0 } }),
[],
);
useDraggable(controlledRef, () => [currentPosComp]);
useDraggable(initialRef, () => [defaultPosComp]);
return (
<div>
<div ref={controlledRef}>Controlled position</div>
<div ref={initialRef}>Initial position, then user controls</div>
<button onClick={() => setCurrentPos({ x: 200, y: 100 })}>
Move to (200, 100)
</button>
</div>
);
}
Two-Way Binding
import { useState, useRef } from 'react';
import {
position,
events,
useDraggable,
useCompartment,
} from '@neodrag/react';
function SyncedPosition() {
const ref = useRef<HTMLDivElement>(null);
const [elementPosition, setElementPosition] = useState({
x: 50,
y: 50,
});
// Reactive compartments for changing values
const positionComp = useCompartment(
() => position({ current: elementPosition }),
[elementPosition],
);
const eventsComp = useCompartment(
() =>
events({
onDrag: (data) => {
setElementPosition(data.offset);
localStorage.setItem(
'elementPosition',
JSON.stringify(data.offset),
);
},
}),
[setElementPosition],
);
useDraggable(ref, () => [positionComp, eventsComp]);
return (
<div>
<div ref={ref}>Position synced with state</div>
<p>
Current position: {elementPosition.x}, {elementPosition.y}
</p>
</div>
);
}
Common Use Cases
Animated Positioning
function animateToPosition(targetX, targetY, duration = 500) {
const startPos = getCurrentPosition();
const startTime = Date.now();
function animate() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3); // Ease out cubic
const currentX = startPos.x + (targetX - startPos.x) * ease;
const currentY = startPos.y + (targetY - startPos.y) * ease;
// Update compartment with new position
positionComp.current = position({
current: { x: currentX, y: currentY },
});
if (progress < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
Multi-Element Coordination
const formation = [
{ id: 'leader', offset: { x: 0, y: 0 } },
{ id: 'follower1', offset: { x: 50, y: 0 } },
{ id: 'follower2', offset: { x: 25, y: 50 } },
];
// Use compartments for each element's position
const leaderPosComp = new Compartment(() =>
position({ current: leaderPos }),
);
const follower1PosComp = new Compartment(() =>
position({ current: follower1Pos }),
);
const follower2PosComp = new Compartment(() =>
position({ current: follower2Pos }),
);
const leaderEventsComp = new Compartment(() =>
events({
onDrag: (data) => {
leaderPos = data.offset;
// Update followers
formation.forEach((item) => {
if (item.id !== 'leader') {
const followerPos = {
x: data.offset.x + item.offset.x,
y: data.offset.y + item.offset.y,
};
updateElementPosition(item.id, followerPos);
}
});
},
}),
);
Snap to Targets
const targets = [
{ x: 100, y: 100 },
{ x: 200, y: 150 },
{ x: 300, y: 50 },
];
const snapEventsComp = new Compartment(() =>
events({
onDragEnd: (data) => {
const snapDistance = 20;
const nearest = targets.find((target) => {
const distance = Math.sqrt(
Math.pow(data.offset.x - target.x, 2) +
Math.pow(data.offset.y - target.y, 2),
);
return distance <= snapDistance;
});
if (nearest) {
// Update position compartment to snap to target
positionComp.current = position({ current: nearest });
}
},
}),
);
Combining with Other Plugins
// Position runs first, then other plugins modify
// Static plugins can be used directly
const staticPlugins = [
grid([20, 20]), // Grid snapping
bounds(BoundsFrom.parent()), // Bounds checking
];
// Reactive position needs compartment
const positionComp = new Compartment(() =>
position({ current: { x: 100, y: 50 } }),
);
// Combine in plugins array
const plugins = () => [
positionComp, // High priority - runs first
...staticPlugins,
];
Position plugin has high priority (1000) ensuring it sets the base position before other plugins modify movement.
How It Works
The position plugin:
- Runs in setup when not currently interacting
- Uses
setForcedPosition()
to immediately jump to coordinates - Checks for changes - only updates if position actually changed
- High priority - runs before other position-modifying plugins
- Live updates - can change position during active drags
setup(ctx) {
if (!ctx.isInteracting) {
const x = options?.current?.x ?? options?.default?.x ?? ctx.offset.x;
const y = options?.current?.y ?? options?.default?.y ?? ctx.offset.y;
if (x !== ctx.offset.x || y !== ctx.offset.y) {
ctx.setForcedPosition(x, y);
}
}
}
API Reference
function position(
options?: {
current?: { x: number; y: number } | null;
default?: { x: number; y: number } | null;
} | null,
): Plugin;
Options:
current
- Force element to this position (overrides dragging)default
- Set initial position (user can still drag normally)
Behavior:
current
takes precedence overdefault
- Only updates if position actually changed
- Runs with high priority (1000)
- Supports live updates during dragging
Returns: A plugin object for use with draggable.
Important Notes
- Use compartments for reactive values: When position values change, wrap the position plugin in a compartment
- Solid.js array format: Solid uses
[compartment]
while other frameworks use() => [compartment]
- Manual updates in vanilla: Don’t forget to update compartments manually in vanilla JavaScript
- Static positions: For positions that never change, you can use the plugin directly without compartments