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
<script>
import { draggable, position, Compartment } from '@neodrag/svelte';
let currentPos = $state({ x: 100, y: 50 });
let defaultPos = $state({ x: 0, y: 0 });
// Reactive compartments for changing values
const currentPosComp = Compartment.of(() =>
position({ current: currentPos }),
);
const defaultPosComp = Compartment.of(() =>
position({ default: defaultPos }),
);
</script>
<!-- Force element to specific position -->
<div {@attach draggable(() => [currentPosComp])}>
Controlled position
</div>
<!-- Set initial position only -->
<div {@attach draggable(() => [defaultPosComp])}>
Initial position, then user controls
</div>
Two-Way Binding
<script>
import {
draggable,
position,
events,
Compartment,
} from '@neodrag/svelte';
let elementPosition = $state({ x: 50, y: 50 });
// Reactive compartments for changing values
const positionComp = Compartment.of(() =>
position({ current: elementPosition }),
);
const eventsComp = Compartment.of(() =>
events({
onDrag: (data) => {
elementPosition = data.offset;
savePosition(elementPosition);
},
}),
);
// Save position to localStorage
function savePosition(pos) {
localStorage.setItem('elementPosition', JSON.stringify(pos));
}
</script>
<div {@attach draggable(() => [positionComp, eventsComp])}>
Position synced with state
</div>
<p>Current position: {elementPosition.x}, {elementPosition.y}</p>
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