@svelte-put/movable
GithubCompatible with or powered directly by Svelte runes.
Installation
npm install --save-dev @svelte-put/movable
pnpm add -D @svelte-put/movable
yarn add -D @svelte-put/movable
New to Svelte 5? See Migration Guides.
Quick Start
In the following minimal example, try dragging the blue box around.
<script lang="ts">
import { movable } from '@svelte-put/movable';
</script>
<div class="hl-info z-overlay grid h-20 w-20 place-items-center" use:movable>...</div>
Events
<div use:movable onmovablestart onmovableend />
movable
provides two CustomEvents, movablestart
and movableend
, with event.detail
as MovableEventDetail
.
export interface MovableEventDetail {
/** the node that the action was placed on */
node: HTMLElement;
/** last known position, as in styles.position */
position: {
/** styles.position.left */
left: number;
/** styles.position.right */
top: number;
};
}
Setting Limit
By default, as seen in Quick Start, node that uses movable
can be moved freely. The limit
option can be set to limit the "movable" zone.
Limit within Screen
<div use:movable={{ limit: { parent: 'screen' } }} />
Limit to the viewport by setting limit.parent
to 'screen'
.
<script>
import { movable } from '@svelte-put/movable';
</script>
<div
class="hl-info z-overlay grid h-20 w-20 place-items-center"
use:movable={{ limit: { parent: 'screen' } }}
>
...
</div>
HTMLElement
Ancestor
Limit within an <div use:movable={{ limit: { parent: ancestorNode } }} />
Limit to an ancestor by referencing limit.parent
to that ancestor.
Box below can be moved around, but only within the violet border
<script lang="ts">
import { movable } from '@svelte-put/movable';
let parentNode: HTMLElement;
</script>
<div class="grid place-items-center border-2 border-violet-500 p-4" bind:this={parentNode}>
<p class="text-center">Box below can be moved around, but only within the violet border</p>
<div
class="hl-info grid h-20 w-20 place-items-center"
use:movable={{ limit: { parent: parentNode } }}
>
...
</div>
</div>
movable
cannot take into account scrollbars. So, for example, if your limit.parent
is a scroll container with visible vertical scrollbar, moving the "movable" node to the scrollbar region will cause overflow in the horizontal direction, triggering the horizontal scrollbar to appear.
Limit within Delta
<div use:movable={{ limit: { delta: '50%' } }} />
<div use:movable={{ limit: { delta: '250px' } }} />
<div use:movable={{ limit: { delta: { x: '20%', y: '100px' } } }} />
Set limit.delta
to limit movable
to a certain travel distance. It takes a number
with unit of %
or px
in one or both axes, and can be set insolation or in combination with limit.parent
.
<script>
import { movable } from '@svelte-put/movable';
</script>
<div class="hl-info grid h-20 w-20 place-items-center" use:movable={{ limit: { delta: '100%' } }}>
...
</div>
Limit to Single Axis
<div use:movable={{ limit: { delta: { x: 0 } } }} />
<div use:movable={{ limit: { delta: { y: 0 } } }} />
Setting limit.delta.x
or limit.delta.y
to 0
effectively disables movement in that axis.
<script>
import { movable } from '@svelte-put/movable';
</script>
<div
class="hl-info grid h-20 w-20 place-items-center"
use:movable={{ limit: { delta: { x: '100%', y: 0 } } }}
>
...
</div>
Custom Handle
Handle is an HTMLElement on which movable
will listen for pointer events to initiate and map movement to the target node.
<div use:movable={{ handle: someNode }} />
By default, the target node itself is the handle. To use a custom handle, set handle
to the desired HTMLElement.
<script lang="ts">
import { movable } from '@svelte-put/movable';
let handle: HTMLElement | undefined = $state();
</script>
<div class="hl-info flex h-40 w-40 flex-col p-2" use:movable={{ handle }}>
<div class="hl-warning grid h-8 w-8 place-items-center self-end" bind:this={handle}>.</div>
<div class="grid flex-1 place-items-center self-stretch">...</div>
</div>
movable
on Click
Ignore Children Nodes from Triggering Prefer handle
in favor of ignore
when possible for better predictability.
The ignore
option takes one or more CSS selector strings and will prevent matching children of handle
from triggering movable
.
<script>
import { movable } from '@svelte-put/movable';
import { onMount } from 'svelte';
let mounted = false;
onMount(() => {
setTimeout(() => {
mounted = true;
}, 500);
});
</script>
<div
class="hl-info z-overlay grid h-40 w-40 place-items-center"
use:movable={{ ignore: '.to-ignore' }}
>
{#if mounted}
<p class="to-ignore hl-error cursor-auto p-2 text-sm">To ignore</p>
{/if}
</div>
Disabling Cursor Handling
<div use:movable={{ cursor: false }} />
By default, movable
adds cursor: grab
to handle
, and cursor: grabbing
on mousedown
to both handle
and window.body
. This can be disabled by setting the cursor
option to false
.
Automatic cursor handling is done on action initialization and update. It won't work for ignored children that are dynamically added to DOM at runtime. In such cases, disable cursor
or add cursor styles manually to the ignored children.
Migration Guides
V3 -> V4 (Svelte 5 in Runes mode)
When migrating from v3 to v4, you just need to update event attribute syntax to remove colon :
:
<div use:movable on:movablestart on:movableend></div>
<div use:movable onmovablestart onmovableend></div>
Happy moving node! 👨💻