An action based drag and drop container for Svelte
JavaScript
2,126
540 commits
updated Oct 5, 2026
This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case, any input device and is fully accessible.
It requires very minimal configuration, while offering a rich set of primitives that allow overriding basically any of its default behaviours (using the handler functions).
See full features list below.

Play with this example in the REPL.
The library is production ready, and I am in the process of integrating it into several production systems that will be used at scale. It is being actively maintained. I am doing my best to avoid breaking-changes and keep the API stable.
A custom action allows for a much more elegant API (no slot props thanks god) as well as more control.
If you prefer a generic dnd list component that accepts different child components as your abstraction, you can very easily wrap this library with one (see here).
Pre-requisites: svelte-3 (>=3.23.0)
yarn add -D svelte-dnd-action
or
npm install --save-dev svelte-dnd-action
<div use:dndzone="{{items: myItems, ...otherOptions}}" on:consider="{handler}" on:finalize="{handler}">
{#each myItems as item(item.id)}
<div>this is now a draggable div that can be dropped in other dnd zones</div>
{/each}
</div>
<script>
import {flip} from "svelte/animate";
import {dndzone} from "svelte-dnd-action";
let items = [
{id: 1, name: "item1"},
{id: 2, name: "item2"},
{id: 3, name: "item3"},
{id: 4, name: "item4"}
];
const flipDurationMs = 300;
function handleDndConsider(e) {
items = e.detail.items;
}
function handleDndFinalize(e) {
items = e.detail.items;
}
</script>
<style>
section {
width: 50%;
padding: 0.3em;
border: 1px solid black;
/* this will allow the dragged element to scroll the list although starting in version 0.9.41 the lib would detect any scrollable parent*/
overflow: scroll;
height: 200px;
}
div {
width: 50%;
padding: 0.2em;
border: 1px solid blue;
margin: 0.15em 0;
}
</style>
<section use:dndzone="{{items, flipDurationMs}}" on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}">
{#each items as item(item.id)}
<div animate:flip="{{duration: flipDurationMs}}">{item.name}</div>
{/each}
</section>
An options-object with the following attributes:
| Name | Type | Required? | Default Value | Description |
|---|---|---|---|---|
items | Array<Object> | Yes. Each object in the array has to have an id property (key name can be overridden globally) with a unique value (within all dnd-zones of the same type) | N/A | The data array that is used to produce the list with the draggable items (the same thing you run your #each block on). The dndzone should not have children that don't originate in items |
flipDurationMs | Number | No | 0 | The same value you give the flip animation on the items (to make them animated as they "make space" for the dragged item). Set to zero if you dont want animations, if unset it defaults to 100ms |
type | String | No | Internal | dnd-zones that share the same type can have elements from one dragged into another. By default, all dnd-zones have the same type |
dragDisabled | Boolean | No | false | Setting it to true will make it impossible to drag elements out of the dnd-zone. You can change it at any time, and the zone will adjust on the fly |
morphDisabled | Boolean | No | false | By default, when dragging over a zone, the dragged element is morphed to look like it would if dropped. You can prevent it by setting this option. |
dropFromOthersDisabled | Boolean | No | false | Setting it to true will make it impossible to drop elements from other dnd-zones of the same type. Can be useful if you want to limit the max number of items for example. You can change it at any time, and the zone will adjust on the fly |
zoneTabIndex | Number | No | 0 | Allow user to set custom tabindex to the list container when not dragging. Can be useful if you want to make the screen reader to skip the list container. You can change it at any time. |
zoneItemTabIndex | Number | No | 0 | Allow user to set custom tabindex to the list container items when not dragging. Can be useful if you use Drag handles. You can change it at any time. |
dropTargetStyle | Object<String> | No | {outline: 'rgba(255, 255, 102, 0.7) solid 2px'} | An object of styles to apply to the dnd-zone when items can be dragged into it. Note: the styles override any inline styles applied to the dnd-zone. When the styles are removed, any original inline styles will be lost |
dropTargetClasses | Array<String> | No | [] | A list of classes to apply to the dnd-zone when items can be dragged into it. Note: make sure the classes you use are global. |
transformDraggedElement | Function | No | () => {} | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. Signature: function(element, data, index) {} element: The dragged element. data: The data of the item from the items array. index: The index the dragged element will become in the new dnd-zone. This allows you to override properties on the dragged element, such as innerHTML to change how it displays. If what you are after is altering styles, do it to the children, not to the dragged element itself |
autoAriaDisabled | Boolean | No | false | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard). Use it only if you intend to implement your own custom instructions, roles and alerts. In such a case, you might find the exported function alertToScreenReader(string, document?) useful (pass the zone's document when the zone is in another window, such as an iframe or a popup). If you only want to change the wording (ex: to translate it), use setAriaStrings instead and leave this off. |
centreDraggedOnCursor | Boolean | No | false | Setting it to true will cause elements from this dnd-zone to position their center on the cursor on drag start, effectively turning the cursor to the focal point that triggers all the dnd events (ex: entering another zone). Useful for dnd-zones with large items that can be dragged over small items. |
useCursorForDetection | Boolean | No | false | Setting it to true will use the cursor position instead of the dragged element's center for drop zone detection. This improves accuracy when dragging large elements over small drop targets. Unlike centreDraggedOnCursor, this option does not reposition the dragged element. |
dropAnimationDisabled | Boolean | No | false | Setting it to true will disable the animation of the dropped element to its final place. |
delayTouchStart | Boolean | Number | No | false | Prevents accidental drags on touch devices that should have been scrolls. |
• true – enable with sensible default (80 ms). | ||||
• number – custom delay in milliseconds. | ||||
• false / omitted – legacy behaviour (drag starts immediately). |
The action dispatches two custom events:
consider - dispatched whenever the dragged element needs to make room for itself in a new position in the items list and when it leaves. The host (your component) is expected to update the items list (you can keep a copy of the original list if you need to)finalize - dispatched on the target and origin dnd-zones when the dragged element is dropped into position. This is the event you want to use to save the items to the server for example.The expectation is the same for both event handlers - update the list of items.
In both cases the payload (within e.detail) is the same: an object with two attributes: items and info.
items: contains the updated items list.info: This one can be used to achieve very advanced custom behaviours (ex: copy on drag). In most cases, don't worry about it. It is an object with the following properties:
trigger: will be one of the exported list of TRIGGERS (Please import if you plan to use): [DRAG_STARTED, DRAGGED_ENTERED, DRAGGED_ENTERED_ANOTHER, DRAGGED_OVER_INDEX, DRAGGED_LEFT, DRAGGED_LEFT_ALL, DROPPED_INTO_ZONE, DROPPED_INTO_ANOTHER, DROPPED_OUTSIDE_OF_ANY, DRAG_STOPPED]. Most triggers apply to both pointer and keyboard, but some are only relevant for pointer (dragged_entered, dragged_over_index and dragged_left), and some only for keyboard (drag_stopped).id: the item id of the dragged elementsource: will be one of the exported list of SOURCES (Please import if you plan to use): [POINTER, KEYBOARD]You have to listen for both events and update the list of items in order for this library to work correctly.
For advanced use-cases (ex: custom styling for the placeholder element) you might also need to import SHADOW_ITEM_MARKER_PROPERTY_NAME, which marks the placeholder element that is temporarily added to the list the dragged element hovers over.
For use cases that have recursively nested zones (ex: crazy nesting), you might want to import SHADOW_PLACEHOLDER_ITEM_ID in order to filter the placeholder out when passing the items in to the nested component.
If you need to manipulate the dragged element either dynamically (and don't want to use the transformDraggedElement option), or statically targeting it or its children with CSS, you can import and use DRAGGED_ELEMENT_ID;
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, please add aria-label on the container and on every draggable item. The library will take care of the rest.
For example:
<h2>{listName}</h2>
<section aria-label="{listName}" use:dndzone="{{items, flipDurationMs}}" on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}">
{#each items as item(item.id)}
<div aria-label="{item.name}" animate:flip="{{duration: flipDurationMs}}">{item.name}</div>
{/each}
</section>
If you don't provide the aria-labels everything will still work, but the messages to the user will be less informative.
Note: in general you probably want to use semantic-html (ex: ol and li elements rather than section and div) but the library is screen readers friendly regardless (or at least that's the goal :)).
If you want to implement your own custom screen-reader alerts, roles and instructions, you can use the autoAriaDisabled options and wire everything up yourself using markup and the consider and finalize handlers (for example: unsortable list).
By default both Space and Enter start and stop a keyboard drag. If your app needs one of those keys for something else - ex: Enter to open or edit the focused item - narrow the trigger:
import {setKeyboardDragTrigger} from "svelte-dnd-action";
setKeyboardDragTrigger("space");
Accepted values are "space", "enter" and "space_or_enter" (the default). Pass null or
undefined (or call with no argument) to restore the default. Any other value throws.
Keys outside the trigger are never claimed. Escape still ends a drag under every trigger.
The setting is global to the document and applies to every dndzone; different zones cannot use different triggers at the same time. It can be called at any time, including after the zones have rendered. The built-in screen-reader instruction follows it automatically.
autoAriaDisabled disables all automatically added ARIA attributes, roles, instructions and alerts. If you only want to translate the screen-reader wording, import setAriaStrings and leave autoAriaDisabled off:
import {setAriaStrings} from "svelte-dnd-action";
const instructionsParTouche = {
space: `Tabulez jusqu'à un élément et appuyez sur espace pour le déplacer`,
enter: `Tabulez jusqu'à un élément et appuyez sur entrée pour le déplacer`,
space_or_enter: `Tabulez jusqu'à un élément et appuyez sur espace ou entrée pour le déplacer`
};
setAriaStrings({
dragStarted: ({itemLabel, zoneLabel, canMoveBetweenZones}) =>
`Déplacement de ${itemLabel} commencé. Utilisez les flèches pour le déplacer dans la liste ${zoneLabel}` +
(canMoveBetweenZones ? `, ou tabulez vers une autre liste` : ``),
movedToPosition: ({itemLabel, zoneLabel, position}) => `${itemLabel} déplacé en position ${position} dans la liste ${zoneLabel}`,
movedToZoneEnd: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé à la fin de la liste ${zoneLabel}`,
movedToZoneStart: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé au début de la liste ${zoneLabel}`,
// The default message omits the destination, but custom messages can include it
dropped: ({itemLabel, zoneLabel, position, count}) => `${itemLabel} déposé dans ${zoneLabel}, ${position} sur ${count}`,
zoneActiveInstruction: ({keyboardDragTrigger}) => instructionsParTouche[keyboardDragTrigger],
zoneDragDisabledInstruction: `Cette liste de glisser-déposer est désactivée`
});
Every key is optional. Omitted keys use their English defaults. The five announcement keys are formatter
functions, so translations can control word order and pluralisation. Each formatter receives itemLabel
and zoneLabel from the consumer-provided aria-label attributes, plus position and count for the
item's 1-based position and the number of items in the relevant zone. dragStarted also receives
canMoveBetweenZones. Destructure only the fields your wording needs; the built-in English messages do not
use every field, but custom messages can.
zoneActiveInstruction takes a string or a formatter receiving the value set through
setKeyboardDragTrigger.
Call setAriaStrings during app-level initialization and again whenever the application locale changes. Each call defines the complete active locale by applying its overrides to the English defaults, so omitted keys return to English rather than retaining values from the previous locale. Existing instruction elements update immediately. Pass null to restore all English defaults. Unknown keys and values of the wrong type throw an error.
The setting is global to the document and applies to every dndzone. Different zones cannot use different strings at the same time.
setKeyboardDragTrigger to narrow this to one key). The reader will tell the user a drag has started.consider (only on drag start) and finalize (every time the item is moved) events but share only some of the TRIGGERS. The same handlers should work fine for both.Due to popular demand, starting in version 0.9.46 the library exports a wrapper action that greatly improves the ergonomics around using drag handles. Notes:
dragHandleZone would not be draggable unless it has an element that uses the dragHandle action inside (doesn't have to be a direct child but has to be inside the bounding rect of the item).<script>
import {dragHandleZone, dragHandle} from "svelte-dnd-action";
import {flip} from "svelte/animate";
let items = [
{
id: 1,
text: "Item 1"
},
{
id: 2,
text: "Item 2"
},
{
id: 3,
text: "Item 3"
}
];
const flipDurationMs = 100;
function handleSort(e) {
items = e.detail.items;
}
</script>
<style>
div {
position: relative;
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
.handle {
position: absolute;
right: 0;
width: 1em;
height: 0.5em;
background-color: grey;
}
</style>
<h3>Drag Handles</h3>
<p>Items can be dragged using the grey handles via mouse, touch or keyboard. The text on the items can be selected without starting a drag</p>
<hr />
<section use:dragHandleZone="{{ items, flipDurationMs }}" on:consider="{handleSort}" on:finalize="{handleSort}">
{#each items as item (item.id)}
<div animate:flip="{{ duration: flipDurationMs }}">
<div use:dragHandle aria-label="drag-handle for {item.text}" class="handle" />
<span>{item.text}</span>
</div>
{/each}
</section>
Zones in same-origin iframes and popup windows — for the full demo, log in, fork/save and reload, or download/run locally; Playground isolates other documents for visitors.
Board (nested zones and multiple types), scrolling containers, scrolling page
Experimental: make an entire board skippable in the Tab order while preserving keyboard drag and drop — a prototype wrapper built using the existing tabindex options; see the example for current limitations.
Drag handles using wrapper actions, for nested scenarios (same usage, it "just works"), see here and here
Drag handles - legacy, use before version 0.9.46, courtesy of @gleuch
Crazy nesting, courtesy of @zahachtah
Maintaining internal scroll poisition on scrollable dragabble
Select multiple elements to drag (multi-drag) with mouse or keyboard
A dndzone can live in another window than the one that loaded the library: a same-origin iframe, a popup opened with window.open, or another Electron window. Each zone uses its own window's events, timers and document, so drags keep working while the loading window is hidden.
This is useful when one app mounts components into several documents (for example, a pop-out editor). A self-contained iframe that loads its own app and imports the library there could already use it without this feature. The difference is that the code can now be loaded in one document while its zones live in another; it does not add dragging between documents.
See the official example REPL and its source and setup instructions. For iframe/popup interaction inside Playground, fork/save and reload under your account, or download and run locally.
alertToScreenReader(text, zone.ownerDocument).finalize event (pointer) or consider event with the dragStopped trigger (keyboard).items) MUST HAVE AN ID PROPERTY CALLED id. You can override it globally if you'd like to use a different key (see below)items array that is passed in.Sometimes it is useful to use a different key for your items instead of id, for example when working with PouchDB which expects _id. It can save some annoying conversions back and forth.
In such cases you can import and call overrideItemIdKeyNameBeforeInitialisingDndZones. This function accepts one parameter of type string which is the new id key name.
For example:
import {overrideItemIdKeyNameBeforeInitialisingDndZones} from "svelte-dnd-action";
overrideItemIdKeyNameBeforeInitialisingDndZones("_id");
It applies globally (as in, all of your items everywhere are expected to have a unique identifier with this name). It can only be called when there are no rendered dndzones (I recommend calling it within the top-level
By default, no debug output will be logged to the console. If you want to see internal debug messages, you can enable the debug output like this:
import {setDebugMode} from "svelte-dnd-action";
setDebugMode(true);
Feature flags allow controlling global optional behaviour. They were originally introduced as a way to enable a workaround for a browser bug that helps in certain scenarios but comes with unwanted side effects in others. In order to set a feature flag use:
import {setFeatureFlag, FEATURE_FLAG_NAMES} from "svelte-dnd-action";
setFeatureFlag(FEATURE_FLAG_NAMES.MY_FLAG, true);
Currently, there is only one flag: USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT, which defaults to false. See issues 454 and 470 for details about why it is needed and when (most users don't need to care about this)
import {setDebugMode} from "svelte-dnd-action";
setDebugMode(true);
TypeScript support has been added since version 0.9.40 and you do not need to set up any custom typings. However, in case you are using some older version or face some types issue, you will need to add the following block to your global.d.ts (at least until this svelte issue is resolved):
declare type Item = import("svelte-dnd-action").Item;
declare type DndEvent<ItemType = Item> = import("svelte-dnd-action").DndEvent<ItemType>;
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
}
}
declare type Item = import("svelte-dnd-action").Item;
declare type DndEvent<ItemType = Item> = import("svelte-dnd-action").DndEvent<ItemType>;
declare namespace svelteHTML {
interface HTMLAttributes<T> {
"on:consider"?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
"on:finalize"?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
}
}
You may need to edit tsconfig.json to include global.d.ts if it doesn't already: "include": ["src/**/*", "global.d.ts"].
Note: If you are using Sveltekit you should use
svelte.config.jsto modify the generatedtsconfig.jsonrather than adding theincludeelement to the roottsconfig.json. Addingincludeto the root file will cause issues because it will override theincludearray defined in.svelte-kit/tsconfig.json. Example:const config = { kit: { typescript: { config(config) { // This path is relative to the ".svelte-kit" folder config.include.push("../global.d.ts"); } } } };
Then you will be able to use the library with type safety as follows (Typescript gurus out there, improvements are welcome 😄):
<style>
section {
width: 12em;
padding: 1em;
height: 7.5em;
}
div {
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
</style>
<script lang="ts">
import {dndzone} from "svelte-dnd-action";
import {flip} from "svelte/animate";
const flipDurationMs = 200;
function handleSort(e: CustomEvent<DndEvent>) {
items = e.detail.items as {id: number; title: string}[];
}
let items = [
{id: 1, title: "I"},
{id: 2, title: "Am"},
{id: 3, title: "Yoda"}
];
</script>
<section use:dndzone="{{items, flipDurationMs}}" on:consider="{handleSort}" on:finalize="{handleSort}">
{#each items as item(item.id)}
<div animate:flip="{{duration:flipDurationMs}}">{item.title}</div>
{/each}
</section>
DndEvent<T>You can use generics to set the type of items you are expecting in DndEvent. Simply add a type to it like so: DndEvent<Dog>. For example:
<script lang="ts">
import {dndzone} from "svelte-dnd-action";
import {flip} from "svelte/animate";
interface Dog {
id: number;
name: string;
breed: string;
}
function handleSort(e: CustomEvent<DndEvent<Dog>>) {
//e.detail.items now evaluates to type Dog.
items = e.detail.items;
}
let items: Dog[] = [
{id: 1, name: "Fido", breed: "bulldog"},
{id: 2, name: "Spot", breed: "labrador"},
{id: 3, name: "Jacky", breed: "golden retriever"}
];
</script>
Svelte 5 prefers onconsider and onfinalize (over on:consider and on:finalize) but works both ways as long as it's consistent within a file.
This is an experimental feature added in version 0.9.29. If you have multiple levels of nesting, the lib might do unnecessary work when dragging an element that has nested zones inside.
Specifically, it allows nested zones within the shadow element (the placeholder under the dragged element) to register and destroy.
This is because Svelte calls nested actions before the parent action (opposite to the rendering order).
You can use a data attribute on the items to help the lib prevent this: data-is-dnd-shadow-item-hint={item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
Starting with version 0.9.42. if you use the hint make sure to include it in the key you provide in your each block e.g:
{#each columnItems as column (`${column._id}${column[SHADOW_ITEM_MARKER_PROPERTY_NAME] ? "_" + column[SHADOW_ITEM_MARKER_PROPERTY_NAME] : ""}`)}
...
{/each}
<script>
import {dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME} from 'svelte-dnd-action';
let items = [];
</script>
<section>
<div use:dndzone={{items}} on:consider={e => items = e.detail.items} on:finalize={e => items = e.detail.items}>
{#each items as item (item.id)}
<div data-is-dnd-shadow-item-hint={item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}>
<h1>{item.title}</h1>
<!--more nested zones go here, can include the attribute on their items as demonstrated above-->
</div>
{/each}
</div>
</section>
There is still quite a lot to do. If you'd like to contribute please get in touch (raise an issue or comment on an existing one). Ideally, be specific about which area you'd like to help with. Thank you for reading :)
If your page is scrollable and you found that quick swipes on touch devices sometimes start a drag instead of scrolling, enable the new delayTouchStart option:
<section use:dndzone={{ items, flipDurationMs, delayTouchStart: true }}>
…
</section>
Setting it to true waits 80 ms before hijacking the gesture; passing a number lets you tune the threshold.
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, please add aria-label on the container and on every draggable item. The library will take care of the rest.
Overall
Maintained
Code review
Latest release
Releases this year
(top 24 of 26)
1,323 followers · starred Mar 2021
503 followers · starred Jan 2021
305 followers · starred Apr 2026
1,932 followers · starred Mar 2024
An action based drag and drop container for Svelte
JavaScript
2,126
540 commits
updated Oct 5, 2026
This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case, any input device and is fully accessible.
It requires very minimal configuration, while offering a rich set of primitives that allow overriding basically any of its default behaviours (using the handler functions).
See full features list below.

Play with this example in the REPL.
The library is production ready, and I am in the process of integrating it into several production systems that will be used at scale. It is being actively maintained. I am doing my best to avoid breaking-changes and keep the API stable.
A custom action allows for a much more elegant API (no slot props thanks god) as well as more control.
If you prefer a generic dnd list component that accepts different child components as your abstraction, you can very easily wrap this library with one (see here).
Pre-requisites: svelte-3 (>=3.23.0)
yarn add -D svelte-dnd-action
or
npm install --save-dev svelte-dnd-action
<div use:dndzone="{{items: myItems, ...otherOptions}}" on:consider="{handler}" on:finalize="{handler}">
{#each myItems as item(item.id)}
<div>this is now a draggable div that can be dropped in other dnd zones</div>
{/each}
</div>
<script>
import {flip} from "svelte/animate";
import {dndzone} from "svelte-dnd-action";
let items = [
{id: 1, name: "item1"},
{id: 2, name: "item2"},
{id: 3, name: "item3"},
{id: 4, name: "item4"}
];
const flipDurationMs = 300;
function handleDndConsider(e) {
items = e.detail.items;
}
function handleDndFinalize(e) {
items = e.detail.items;
}
</script>
<style>
section {
width: 50%;
padding: 0.3em;
border: 1px solid black;
/* this will allow the dragged element to scroll the list although starting in version 0.9.41 the lib would detect any scrollable parent*/
overflow: scroll;
height: 200px;
}
div {
width: 50%;
padding: 0.2em;
border: 1px solid blue;
margin: 0.15em 0;
}
</style>
<section use:dndzone="{{items, flipDurationMs}}" on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}">
{#each items as item(item.id)}
<div animate:flip="{{duration: flipDurationMs}}">{item.name}</div>
{/each}
</section>
An options-object with the following attributes:
| Name | Type | Required? | Default Value | Description |
|---|---|---|---|---|
items | Array<Object> | Yes. Each object in the array has to have an id property (key name can be overridden globally) with a unique value (within all dnd-zones of the same type) | N/A | The data array that is used to produce the list with the draggable items (the same thing you run your #each block on). The dndzone should not have children that don't originate in items |
flipDurationMs | Number | No | 0 | The same value you give the flip animation on the items (to make them animated as they "make space" for the dragged item). Set to zero if you dont want animations, if unset it defaults to 100ms |
type | String | No | Internal | dnd-zones that share the same type can have elements from one dragged into another. By default, all dnd-zones have the same type |
dragDisabled | Boolean | No | false | Setting it to true will make it impossible to drag elements out of the dnd-zone. You can change it at any time, and the zone will adjust on the fly |
morphDisabled | Boolean | No | false | By default, when dragging over a zone, the dragged element is morphed to look like it would if dropped. You can prevent it by setting this option. |
dropFromOthersDisabled | Boolean | No | false | Setting it to true will make it impossible to drop elements from other dnd-zones of the same type. Can be useful if you want to limit the max number of items for example. You can change it at any time, and the zone will adjust on the fly |
zoneTabIndex | Number | No | 0 | Allow user to set custom tabindex to the list container when not dragging. Can be useful if you want to make the screen reader to skip the list container. You can change it at any time. |
zoneItemTabIndex | Number | No | 0 | Allow user to set custom tabindex to the list container items when not dragging. Can be useful if you use Drag handles. You can change it at any time. |
dropTargetStyle | Object<String> | No | {outline: 'rgba(255, 255, 102, 0.7) solid 2px'} | An object of styles to apply to the dnd-zone when items can be dragged into it. Note: the styles override any inline styles applied to the dnd-zone. When the styles are removed, any original inline styles will be lost |
dropTargetClasses | Array<String> | No | [] | A list of classes to apply to the dnd-zone when items can be dragged into it. Note: make sure the classes you use are global. |
transformDraggedElement | Function | No | () => {} | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. Signature: function(element, data, index) {} element: The dragged element. data: The data of the item from the items array. index: The index the dragged element will become in the new dnd-zone. This allows you to override properties on the dragged element, such as innerHTML to change how it displays. If what you are after is altering styles, do it to the children, not to the dragged element itself |
autoAriaDisabled | Boolean | No | false | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard). Use it only if you intend to implement your own custom instructions, roles and alerts. In such a case, you might find the exported function alertToScreenReader(string, document?) useful (pass the zone's document when the zone is in another window, such as an iframe or a popup). If you only want to change the wording (ex: to translate it), use setAriaStrings instead and leave this off. |
centreDraggedOnCursor | Boolean | No | false | Setting it to true will cause elements from this dnd-zone to position their center on the cursor on drag start, effectively turning the cursor to the focal point that triggers all the dnd events (ex: entering another zone). Useful for dnd-zones with large items that can be dragged over small items. |
useCursorForDetection | Boolean | No | false | Setting it to true will use the cursor position instead of the dragged element's center for drop zone detection. This improves accuracy when dragging large elements over small drop targets. Unlike centreDraggedOnCursor, this option does not reposition the dragged element. |
dropAnimationDisabled | Boolean | No | false | Setting it to true will disable the animation of the dropped element to its final place. |
delayTouchStart | Boolean | Number | No | false | Prevents accidental drags on touch devices that should have been scrolls. |
• true – enable with sensible default (80 ms). | ||||
• number – custom delay in milliseconds. | ||||
• false / omitted – legacy behaviour (drag starts immediately). |
The action dispatches two custom events:
consider - dispatched whenever the dragged element needs to make room for itself in a new position in the items list and when it leaves. The host (your component) is expected to update the items list (you can keep a copy of the original list if you need to)finalize - dispatched on the target and origin dnd-zones when the dragged element is dropped into position. This is the event you want to use to save the items to the server for example.The expectation is the same for both event handlers - update the list of items.
In both cases the payload (within e.detail) is the same: an object with two attributes: items and info.
items: contains the updated items list.info: This one can be used to achieve very advanced custom behaviours (ex: copy on drag). In most cases, don't worry about it. It is an object with the following properties:
trigger: will be one of the exported list of TRIGGERS (Please import if you plan to use): [DRAG_STARTED, DRAGGED_ENTERED, DRAGGED_ENTERED_ANOTHER, DRAGGED_OVER_INDEX, DRAGGED_LEFT, DRAGGED_LEFT_ALL, DROPPED_INTO_ZONE, DROPPED_INTO_ANOTHER, DROPPED_OUTSIDE_OF_ANY, DRAG_STOPPED]. Most triggers apply to both pointer and keyboard, but some are only relevant for pointer (dragged_entered, dragged_over_index and dragged_left), and some only for keyboard (drag_stopped).id: the item id of the dragged elementsource: will be one of the exported list of SOURCES (Please import if you plan to use): [POINTER, KEYBOARD]You have to listen for both events and update the list of items in order for this library to work correctly.
For advanced use-cases (ex: custom styling for the placeholder element) you might also need to import SHADOW_ITEM_MARKER_PROPERTY_NAME, which marks the placeholder element that is temporarily added to the list the dragged element hovers over.
For use cases that have recursively nested zones (ex: crazy nesting), you might want to import SHADOW_PLACEHOLDER_ITEM_ID in order to filter the placeholder out when passing the items in to the nested component.
If you need to manipulate the dragged element either dynamically (and don't want to use the transformDraggedElement option), or statically targeting it or its children with CSS, you can import and use DRAGGED_ELEMENT_ID;
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, please add aria-label on the container and on every draggable item. The library will take care of the rest.
For example:
<h2>{listName}</h2>
<section aria-label="{listName}" use:dndzone="{{items, flipDurationMs}}" on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}">
{#each items as item(item.id)}
<div aria-label="{item.name}" animate:flip="{{duration: flipDurationMs}}">{item.name}</div>
{/each}
</section>
If you don't provide the aria-labels everything will still work, but the messages to the user will be less informative.
Note: in general you probably want to use semantic-html (ex: ol and li elements rather than section and div) but the library is screen readers friendly regardless (or at least that's the goal :)).
If you want to implement your own custom screen-reader alerts, roles and instructions, you can use the autoAriaDisabled options and wire everything up yourself using markup and the consider and finalize handlers (for example: unsortable list).
By default both Space and Enter start and stop a keyboard drag. If your app needs one of those keys for something else - ex: Enter to open or edit the focused item - narrow the trigger:
import {setKeyboardDragTrigger} from "svelte-dnd-action";
setKeyboardDragTrigger("space");
Accepted values are "space", "enter" and "space_or_enter" (the default). Pass null or
undefined (or call with no argument) to restore the default. Any other value throws.
Keys outside the trigger are never claimed. Escape still ends a drag under every trigger.
The setting is global to the document and applies to every dndzone; different zones cannot use different triggers at the same time. It can be called at any time, including after the zones have rendered. The built-in screen-reader instruction follows it automatically.
autoAriaDisabled disables all automatically added ARIA attributes, roles, instructions and alerts. If you only want to translate the screen-reader wording, import setAriaStrings and leave autoAriaDisabled off:
import {setAriaStrings} from "svelte-dnd-action";
const instructionsParTouche = {
space: `Tabulez jusqu'à un élément et appuyez sur espace pour le déplacer`,
enter: `Tabulez jusqu'à un élément et appuyez sur entrée pour le déplacer`,
space_or_enter: `Tabulez jusqu'à un élément et appuyez sur espace ou entrée pour le déplacer`
};
setAriaStrings({
dragStarted: ({itemLabel, zoneLabel, canMoveBetweenZones}) =>
`Déplacement de ${itemLabel} commencé. Utilisez les flèches pour le déplacer dans la liste ${zoneLabel}` +
(canMoveBetweenZones ? `, ou tabulez vers une autre liste` : ``),
movedToPosition: ({itemLabel, zoneLabel, position}) => `${itemLabel} déplacé en position ${position} dans la liste ${zoneLabel}`,
movedToZoneEnd: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé à la fin de la liste ${zoneLabel}`,
movedToZoneStart: ({itemLabel, zoneLabel}) => `${itemLabel} déplacé au début de la liste ${zoneLabel}`,
// The default message omits the destination, but custom messages can include it
dropped: ({itemLabel, zoneLabel, position, count}) => `${itemLabel} déposé dans ${zoneLabel}, ${position} sur ${count}`,
zoneActiveInstruction: ({keyboardDragTrigger}) => instructionsParTouche[keyboardDragTrigger],
zoneDragDisabledInstruction: `Cette liste de glisser-déposer est désactivée`
});
Every key is optional. Omitted keys use their English defaults. The five announcement keys are formatter
functions, so translations can control word order and pluralisation. Each formatter receives itemLabel
and zoneLabel from the consumer-provided aria-label attributes, plus position and count for the
item's 1-based position and the number of items in the relevant zone. dragStarted also receives
canMoveBetweenZones. Destructure only the fields your wording needs; the built-in English messages do not
use every field, but custom messages can.
zoneActiveInstruction takes a string or a formatter receiving the value set through
setKeyboardDragTrigger.
Call setAriaStrings during app-level initialization and again whenever the application locale changes. Each call defines the complete active locale by applying its overrides to the English defaults, so omitted keys return to English rather than retaining values from the previous locale. Existing instruction elements update immediately. Pass null to restore all English defaults. Unknown keys and values of the wrong type throw an error.
The setting is global to the document and applies to every dndzone. Different zones cannot use different strings at the same time.
setKeyboardDragTrigger to narrow this to one key). The reader will tell the user a drag has started.consider (only on drag start) and finalize (every time the item is moved) events but share only some of the TRIGGERS. The same handlers should work fine for both.Due to popular demand, starting in version 0.9.46 the library exports a wrapper action that greatly improves the ergonomics around using drag handles. Notes:
dragHandleZone would not be draggable unless it has an element that uses the dragHandle action inside (doesn't have to be a direct child but has to be inside the bounding rect of the item).<script>
import {dragHandleZone, dragHandle} from "svelte-dnd-action";
import {flip} from "svelte/animate";
let items = [
{
id: 1,
text: "Item 1"
},
{
id: 2,
text: "Item 2"
},
{
id: 3,
text: "Item 3"
}
];
const flipDurationMs = 100;
function handleSort(e) {
items = e.detail.items;
}
</script>
<style>
div {
position: relative;
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
.handle {
position: absolute;
right: 0;
width: 1em;
height: 0.5em;
background-color: grey;
}
</style>
<h3>Drag Handles</h3>
<p>Items can be dragged using the grey handles via mouse, touch or keyboard. The text on the items can be selected without starting a drag</p>
<hr />
<section use:dragHandleZone="{{ items, flipDurationMs }}" on:consider="{handleSort}" on:finalize="{handleSort}">
{#each items as item (item.id)}
<div animate:flip="{{ duration: flipDurationMs }}">
<div use:dragHandle aria-label="drag-handle for {item.text}" class="handle" />
<span>{item.text}</span>
</div>
{/each}
</section>
Zones in same-origin iframes and popup windows — for the full demo, log in, fork/save and reload, or download/run locally; Playground isolates other documents for visitors.
Board (nested zones and multiple types), scrolling containers, scrolling page
Experimental: make an entire board skippable in the Tab order while preserving keyboard drag and drop — a prototype wrapper built using the existing tabindex options; see the example for current limitations.
Drag handles using wrapper actions, for nested scenarios (same usage, it "just works"), see here and here
Drag handles - legacy, use before version 0.9.46, courtesy of @gleuch
Crazy nesting, courtesy of @zahachtah
Maintaining internal scroll poisition on scrollable dragabble
Select multiple elements to drag (multi-drag) with mouse or keyboard
A dndzone can live in another window than the one that loaded the library: a same-origin iframe, a popup opened with window.open, or another Electron window. Each zone uses its own window's events, timers and document, so drags keep working while the loading window is hidden.
This is useful when one app mounts components into several documents (for example, a pop-out editor). A self-contained iframe that loads its own app and imports the library there could already use it without this feature. The difference is that the code can now be loaded in one document while its zones live in another; it does not add dragging between documents.
See the official example REPL and its source and setup instructions. For iframe/popup interaction inside Playground, fork/save and reload under your account, or download and run locally.
alertToScreenReader(text, zone.ownerDocument).finalize event (pointer) or consider event with the dragStopped trigger (keyboard).items) MUST HAVE AN ID PROPERTY CALLED id. You can override it globally if you'd like to use a different key (see below)items array that is passed in.Sometimes it is useful to use a different key for your items instead of id, for example when working with PouchDB which expects _id. It can save some annoying conversions back and forth.
In such cases you can import and call overrideItemIdKeyNameBeforeInitialisingDndZones. This function accepts one parameter of type string which is the new id key name.
For example:
import {overrideItemIdKeyNameBeforeInitialisingDndZones} from "svelte-dnd-action";
overrideItemIdKeyNameBeforeInitialisingDndZones("_id");
It applies globally (as in, all of your items everywhere are expected to have a unique identifier with this name). It can only be called when there are no rendered dndzones (I recommend calling it within the top-level
By default, no debug output will be logged to the console. If you want to see internal debug messages, you can enable the debug output like this:
import {setDebugMode} from "svelte-dnd-action";
setDebugMode(true);
Feature flags allow controlling global optional behaviour. They were originally introduced as a way to enable a workaround for a browser bug that helps in certain scenarios but comes with unwanted side effects in others. In order to set a feature flag use:
import {setFeatureFlag, FEATURE_FLAG_NAMES} from "svelte-dnd-action";
setFeatureFlag(FEATURE_FLAG_NAMES.MY_FLAG, true);
Currently, there is only one flag: USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT, which defaults to false. See issues 454 and 470 for details about why it is needed and when (most users don't need to care about this)
import {setDebugMode} from "svelte-dnd-action";
setDebugMode(true);
TypeScript support has been added since version 0.9.40 and you do not need to set up any custom typings. However, in case you are using some older version or face some types issue, you will need to add the following block to your global.d.ts (at least until this svelte issue is resolved):
declare type Item = import("svelte-dnd-action").Item;
declare type DndEvent<ItemType = Item> = import("svelte-dnd-action").DndEvent<ItemType>;
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
}
}
declare type Item = import("svelte-dnd-action").Item;
declare type DndEvent<ItemType = Item> = import("svelte-dnd-action").DndEvent<ItemType>;
declare namespace svelteHTML {
interface HTMLAttributes<T> {
"on:consider"?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
"on:finalize"?: (event: CustomEvent<DndEvent<ItemType>> & {target: EventTarget & T}) => void;
}
}
You may need to edit tsconfig.json to include global.d.ts if it doesn't already: "include": ["src/**/*", "global.d.ts"].
Note: If you are using Sveltekit you should use
svelte.config.jsto modify the generatedtsconfig.jsonrather than adding theincludeelement to the roottsconfig.json. Addingincludeto the root file will cause issues because it will override theincludearray defined in.svelte-kit/tsconfig.json. Example:const config = { kit: { typescript: { config(config) { // This path is relative to the ".svelte-kit" folder config.include.push("../global.d.ts"); } } } };
Then you will be able to use the library with type safety as follows (Typescript gurus out there, improvements are welcome 😄):
<style>
section {
width: 12em;
padding: 1em;
height: 7.5em;
}
div {
height: 1.5em;
width: 10em;
text-align: center;
border: 1px solid black;
margin: 0.2em;
padding: 0.3em;
}
</style>
<script lang="ts">
import {dndzone} from "svelte-dnd-action";
import {flip} from "svelte/animate";
const flipDurationMs = 200;
function handleSort(e: CustomEvent<DndEvent>) {
items = e.detail.items as {id: number; title: string}[];
}
let items = [
{id: 1, title: "I"},
{id: 2, title: "Am"},
{id: 3, title: "Yoda"}
];
</script>
<section use:dndzone="{{items, flipDurationMs}}" on:consider="{handleSort}" on:finalize="{handleSort}">
{#each items as item(item.id)}
<div animate:flip="{{duration:flipDurationMs}}">{item.title}</div>
{/each}
</section>
DndEvent<T>You can use generics to set the type of items you are expecting in DndEvent. Simply add a type to it like so: DndEvent<Dog>. For example:
<script lang="ts">
import {dndzone} from "svelte-dnd-action";
import {flip} from "svelte/animate";
interface Dog {
id: number;
name: string;
breed: string;
}
function handleSort(e: CustomEvent<DndEvent<Dog>>) {
//e.detail.items now evaluates to type Dog.
items = e.detail.items;
}
let items: Dog[] = [
{id: 1, name: "Fido", breed: "bulldog"},
{id: 2, name: "Spot", breed: "labrador"},
{id: 3, name: "Jacky", breed: "golden retriever"}
];
</script>
Svelte 5 prefers onconsider and onfinalize (over on:consider and on:finalize) but works both ways as long as it's consistent within a file.
This is an experimental feature added in version 0.9.29. If you have multiple levels of nesting, the lib might do unnecessary work when dragging an element that has nested zones inside.
Specifically, it allows nested zones within the shadow element (the placeholder under the dragged element) to register and destroy.
This is because Svelte calls nested actions before the parent action (opposite to the rendering order).
You can use a data attribute on the items to help the lib prevent this: data-is-dnd-shadow-item-hint={item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
Starting with version 0.9.42. if you use the hint make sure to include it in the key you provide in your each block e.g:
{#each columnItems as column (`${column._id}${column[SHADOW_ITEM_MARKER_PROPERTY_NAME] ? "_" + column[SHADOW_ITEM_MARKER_PROPERTY_NAME] : ""}`)}
...
{/each}
<script>
import {dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME} from 'svelte-dnd-action';
let items = [];
</script>
<section>
<div use:dndzone={{items}} on:consider={e => items = e.detail.items} on:finalize={e => items = e.detail.items}>
{#each items as item (item.id)}
<div data-is-dnd-shadow-item-hint={item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}>
<h1>{item.title}</h1>
<!--more nested zones go here, can include the attribute on their items as demonstrated above-->
</div>
{/each}
</div>
</section>
There is still quite a lot to do. If you'd like to contribute please get in touch (raise an issue or comment on an existing one). Ideally, be specific about which area you'd like to help with. Thank you for reading :)
If your page is scrollable and you found that quick swipes on touch devices sometimes start a drag instead of scrolling, enable the new delayTouchStart option:
<section use:dndzone={{ items, flipDurationMs, delayTouchStart: true }}>
…
</section>
Setting it to true waits 80 ms before hijacking the gesture; passing a number lets you tune the threshold.
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, please add aria-label on the container and on every draggable item. The library will take care of the rest.
Overall
Maintained
Code review
Latest release
Releases this year
(top 24 of 26)
1,323 followers · starred Mar 2021
503 followers · starred Jan 2021
305 followers · starred Apr 2026
1,932 followers · starred Mar 2024