position

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:

  1. Runs in setup when not currently interacting
  2. Uses setForcedPosition() to immediately jump to coordinates
  3. Checks for changes - only updates if position actually changed
  4. High priority - runs before other position-modifying plugins
  5. 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 over default
  • 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