@neodrag/svelte

@neodrag/svelte

A lightweight Svelte Attachment to make your elements draggable.

A lightweight Svelte library to make your elements draggable using Svelte 5 attachments or legacy actions.

Installation

npm i @neodrag/svelte@next

Neodrag for Svelte 5 uses the new attachments feature with the {@attach} syntax for better performance and reactivity.

Basic Usage

<script>
  import { draggable } from '@neodrag/svelte';
</script>

<div {@attach draggable()}>Drag me around!</div>

With Plugins

<script>
  import {
    draggable,
    axis,
    grid,
    bounds,
    BoundsFrom,
  } from '@neodrag/svelte';
</script>

<div {@attach draggable([axis('x'), grid([20, 20])])}>
  Horizontal only, snapped to 20px grid
</div>

<div {@attach draggable([bounds(BoundsFrom.parent())])}>
  Constrained to parent element
</div>

Reactive Plugins with Compartments

Svelte 5’s reactivity integrates seamlessly with Neodrag’s compartments:

<script>
  import { draggable, axis, Compartment } from '@neodrag/svelte';

  let currentAxis = $state('x');

  // Compartment automatically updates when currentAxis changes
  const axisCompartment = Compartment.of(() => axis(currentAxis));
</script>

<div {@attach draggable(() => [axisCompartment])}>
  Currently draggable on: {currentAxis} axis
</div>

<button
  onclick={() => (currentAxis = currentAxis === 'x' ? 'y' : 'x')}
>
  Switch to {currentAxis === 'x' ? 'Y' : 'X'} axis
</button>

NOTE: Compartment.of is reactive, its function body is run inside a $effect.pre.

Advanced Examples

Draggable Modal with Handle

<script>
  import {
    draggable,
    controls,
    ControlFrom,
    bounds,
    BoundsFrom,
    events,
  } from '@neodrag/svelte';

  let dragPosition = $state({ x: 0, y: 0 });
</script>

<div
  {@attach draggable([
    controls({ allow: ControlFrom.selector('.drag-handle') }),
    bounds(BoundsFrom.viewport()),
    events({
      onDrag: (data) => {
        dragPosition = data.offset;
      },
    }),
  ])}
>
  <div class="drag-handle">⋮⋮ Drag Handle</div>
  <div>Modal Content</div>
  <div>
    Position: x:{dragPosition.x.toFixed(0)}, y:{dragPosition.y.toFixed(
      0,
    )}
  </div>
</div>

Dynamic Grid Snapping

<script>
  import {
    draggable,
    grid,
    axis,
    Compartment,
  } from '@neodrag/svelte';

  let gridSize = $state(20);
  let lockAxis = $state('both');

  const gridCompartment = Compartment.of(() =>
    grid([gridSize, gridSize]),
  );
  const axisCompartment = Compartment.of(() => axis(lockAxis));
</script>

<label>
  Grid size: {gridSize}px
  <input
    type="range"
    bind:value={gridSize}
    min="10"
    max="50"
    step="5"
  />
</label>

<label>
  Movement axis:
  <select bind:value={lockAxis}>
    <option value="both">Both X & Y</option>
    <option value="x">X only</option>
    <option value="y">Y only</option>
  </select>
</label>

<div {@attach draggable(() => [gridCompartment, axisCompartment])}>
  Grid: {gridSize}px, Axis: {lockAxis}
</div>

Card Stack with Events

<script>
  import {
    draggable,
    events,
    bounds,
    BoundsFrom,
  } from '@neodrag/svelte';

  let cards = $state([
    { id: 1, title: 'Task 1' },
    { id: 2, title: 'Task 2' },
    { id: 3, title: 'Task 3' },
  ]);

  let dragState = $state(null);
</script>

{#each cards as card (card.id)}
  <div
    {@attach draggable([
      bounds(BoundsFrom.parent()),
      events({
        onDragStart: () => {
          dragState = { cardId: card.id, isDragging: true };
        },
        onDrag: (data) => {
          dragState = {
            cardId: card.id,
            position: data.offset,
          };
        },
        onDragEnd: () => {
          dragState = null;
        },
      }),
    ])}
  >
    {card.title}
    {#if dragState?.cardId === card.id}
      - Dragging at ({dragState.position?.x.toFixed(0)}, {dragState.position?.y.toFixed(
        0,
      )})
    {/if}
  </div>
{/each}

{#if dragState}
  <p>Currently dragging card {dragState.cardId}</p>
{/if}

TypeScript Support

Neodrag provides full TypeScript support for Svelte:

<script lang="ts">
  import {
    draggable,
    axis,
    bounds,
    BoundsFrom,
    type Plugin,
    type DragEventData,
  } from '@neodrag/svelte';

  let dragData = $state<DragEventData | null>(null);

  const plugins: Plugin[] = [
    axis('x'),
    bounds(BoundsFrom.viewport()),
    events({
      onDrag: (data: DragEventData) => {
        dragData = data;
      },
    }),
  ];
</script>

<div {@attach draggable(plugins)}>TypeScript-enabled dragging</div>

Legacy Svelte 3/4 Support

For projects still using Svelte 4, import the legacy action:

Basic Legacy Usage

<script>
  import {
    legacyDraggable,
    axis,
    grid,
  } from '@neodrag/svelte/legacy';
</script>

<!-- ⚠️ Legacy action syntax for Svelte 4 -->
<div use:legacyDraggable={[axis('x'), grid([10, 10])]}>
  Drag me (Svelte 4)
</div>

Legacy with Events

<script>
  import { legacyDraggable, events } from '@neodrag/svelte';

  let position = { x: 0, y: 0 };
</script>

<div
  use:legacyDraggable={[
    events({
      onDrag: (data) => {
        position = data.offset;
      },
    }),
  ]}
>
  Position: {position.x}, {position.y}
</div>

Compartments

Compartment.of is not available in the legacy action, you must manually mutate the compartment

import { legacyDraggable, Compartment } from '@neodrag/svelte/legacy';

let currentAxis = 'x';
const axisCompartment = new Compartment(() => axis(currentAxis));

$: axisCompartment.current = axis(currentAxis);

function toggleAxis() {
  currentAxis = currentAxis === 'x' ? 'y' : 'x';
}

Deprecation Notice

The legacyDraggable action is deprecated and will be removed in Neodrag v4. Please migrate to Svelte 5 and use the new {@attach draggable()} syntax for:

  • Better performance
  • Improved reactivity
  • Conditional application support
  • Component spreading support
  • Future-proof compatibility

Migration from Legacy

Svelte 4 (Legacy):

<div use:legacyDraggable={[axis('x')]}>Drag me</div>

Svelte 5 (Recommended):

<div {@attach draggable([axis('x')])}>Drag me</div>