svelte-dnd-action: drag and drop as an action rather than a list component
An action based drag and drop container for Svelte
At a glance
- What is it?
- A Svelte action that attaches sortable, nestable, touch-capable drag and drop to a plain container element, leaving the markup and the data array entirely in the hands of the caller.
- Who is it for?
- svelte-dnd-action works because it declines to own your data. The array stays in your component, the consider and finalize handlers stay in your component, and the action only computes a proposed order and hands it back.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An action instead of a list component
The central bet of this library is that drag and drop should not own your markup. Most drag and drop libraries ship a component that takes your data and renders children itself, which means the component decides the wrapper element, the class names, and how your item markup arrives through slots. This one attaches to an element you already have. The README gives the reason as an API argument rather than a performance one, and puts it bluntly: a higher order component needs slot props, and the author wanted an API without them. In practice you write your own `<div>` or `<li>` or table row and the library only manages behaviour.
That choice also reverses the usual nesting problem. Items are whatever your `#each` block renders, so a draggable item can be a card with images, a form row with inputs, or a full component. The library never inspects your children beyond measuring and repositioning them, so it does not need to understand your component tree.
The README is candid that this is not the only taste available. If you would rather have a generic list component that accepts different child components as its abstraction, the docs say you can wrap the action in one fairly easily and link a Svelte playground example doing exactly that. That is a narrower claim than most libraries in this space make, because it puts the compromise in your code rather than inside the dependency.
Installed as a build time dependency
Installation is one command, and the README asks for it as a dev dependency because Svelte compiles the action reference into your bundle at build time.
yarn add -D svelte-dnd-actionThe npm equivalent is `npm install --save-dev svelte-dnd-action`. The stated prerequisite is Svelte 3 at version 3.23.0 or newer, which is the point at which the action and the event syntax this library relies on became stable.
The package manifest is worth reading before you commit, because it says a lot about the project's priorities. The runtime `dependencies` object is empty, so there is no transitive supply chain under your list. The `devDependencies` are a build and test toolchain: rollup and a Babel preset for producing the distribution files, eslint and prettier for the lint and format scripts, husky with lint-staged for commit hooks, and Cypress pinned at 15.18.1. The scripts are small enough to read in one glance: `test` is `cypress run`, `build` is `yarn lint && rollup -c`, and `prepublishOnly` runs that build. Linting is therefore part of the release path rather than a suggestion, which is the sort of detail that tells you whether a project treats its own repository as the product.
The one contract that cannot be violated
Attaching the action takes an options object, and one field in it is not optional in any real sense. The `items` array must contain objects that each have an `id` property with a value unique across all zones sharing the same type. The key name can be renamed globally if `id` collides with your domain model, but uniqueness is not negotiable. The library uses that identifier to recognise the same item after it has been dragged across zones and re-rendered, so a duplicated or index-based id produces the class of bug where an item jumps or two elements move as one.
The usage block the README opens with is deliberately minimal:
<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>Two details in those five lines are load bearing. The `#each` block is keyed by `item.id`, matching the requirement above, and the zone's children come only from `items`. The options table states this directly: the dndzone should not have children that do not originate in the items array. Any extra child is a static element the library will try to reason about as a draggable position, and the resulting behaviour is confusing rather than obviously wrong.
Animation is configured through `flipDurationMs`, and the table is precise about a mistake that is easy to make. The value must be the same number you pass to the flip animation on the items, so the placeholder space opening up matches the element sliding into it. Set it to zero to disable animation, and if you leave it unset it defaults to 100ms.
Your component keeps the array, the library proposes an order
The state flow is the part that makes the action approach work, and it is easy to read past. Two events come out of the zone. `consider` fires continuously while an item is being dragged over, and it carries a proposed, partially applied ordering. `finalize` fires once when the drag settles and carries the committed ordering. Both deliver the same shape, an event detail object with an `items` array, and the basic example in the README handles them with two functions that do the same thing.
<script>
import {flip} from "svelte/animate";
import {dndzone} from "svelte-dnd-action";
function handleDndConsider(e) {
items = e.detail.items;
}
function handleDndFinalize(e) {
items = e.detail.items;
}
</script>Assigning `e.detail.items` back to your own variable is the entire integration. Svelte's reactivity then re-renders the `#each` block, the library reads the new positions, and nothing is stored inside the library that you cannot see or override. That is also why the two handlers can be identical in the simple case and diverge in the real one. A common pattern is to ignore `consider` and commit only on `finalize`, so intermediate proposals never reach your state.
Zone types, nesting rules, and what the options table covers
Beyond ordering inside one list, the interesting behaviour is what happens between lists. Zones that share the same `type` can accept each other's items, and by default every zone has the same type, so out of the box two zones are one connected surface. Give a zone a `type` and it becomes a private pool. `dropFromOthersDisabled` closes a zone to incoming drags entirely, which is how you cap a list at a maximum number of items, and `dragDisabled` closes it as a source. Both can be toggled at runtime and the zone adjusts on the fly, so a read only view can disable dragging without remounting.
Nesting is supported in the Trello sense: a draggable container can itself contain draggable elements. The library does not care about the shape of a zone, which is why it can handle horizontal carousels and grids as well as vertical lists. Scrolling follows the same principle, with zones of the relevant type scrolling when the dragged element sits near an edge, and parent scroll containers included in the detection. The README notes that any scrollable parent is now detected automatically rather than needing the overflow style it used to require.
The styling options are where the defaults show their age in a useful way. A valid drop target gets `dropTargetStyle` applied, defaulting to a translucent yellow outline, or `dropTargetClasses` if you prefer classes, with the documented caveat that those classes must be global. The table also warns that the styles override inline styles and that when the styles are removed the original inline styles are lost, which is a real caveat for anyone animating their own border.
Accessibility is listed as fully accessible in beta, and the feature list spells out what that means concretely: keyboard support, aria attributes, and assistive instructions for screen readers. Two tabindex options, `zoneTabIndex` and `zoneItemTabIndex`, exist so you can move focus out of the list container when it is not being dragged, which is how you keep a long reorderable list from swallowing tab order.
What the repository tells you that the README does not
The tree is small and tells you how the project is tested. `src/` holds the implementation, `typings/` the type declarations, `cypress/` the end-to-end suite, and the root carries rollup and babel configuration plus a `release-notes.md`. The Cypress directory is the significant entry, because drag and drop is one of the few browser behaviours that cannot be meaningfully unit tested in isolation: the interesting failures are pointer movement, scroll containers, and hit testing against real layout. Driving a real browser for the test suite is the honest choice, and having the harness in the published repository means you can read what the author considered a regression.
Two gaps are visible from the outside. There are no tagged releases in the repository data and no homepage beyond the GitHub URL in the manifest, so version history lives in `release-notes.md` and in the manifest itself. The manifest reports version 0.9.79, which reads as a long iteration on an unreleased 0.x line rather than a series of named versions.
There is also a small tension worth naming between two parts of the README. The status section says the library is production ready, is being actively maintained, and is being integrated into several production systems by its author, with an explicit effort to avoid breaking changes. The feature list, however, marks accessibility as beta. Both statements can be true at once: the interaction model is settled and long lived while the keyboard and screen reader path is still maturing. The last push to the repository was on 2026-08-21.
Editorial conclusion
svelte-dnd-action works because it declines to own your data. The array stays in your component, the consider and finalize handlers stay in your component, and the action only computes a proposed order and hands it back. The cost of that restraint is a contract you have to respect exactly: every item needs a stable unique id, the zone's children must come from the items array, and the flip duration you configure has to match the animation you attach to the items. Get those three right and the zone behaves like native markup. Start from the basic example in the README, then add types when you need to move items between two lists, and read the options table before reaching for the handler overrides.
Frequently asked questions
How does svelte-dnd-action compare with a generic dnd list component?
The action attaches to a container you write, so your markup and your data stay yours, and item components can be anything. A generic list component renders the children itself and passes state down through slot props. The README argues the action is the more elegant API and notes that if you prefer the component form you can wrap this library in one.
Does every item in the items array need a unique id?
Yes. Each object in the array must have an id property with a value unique across all zones of the same type, and the key name can be changed globally if id does not fit your model. The #each block should be keyed by that same property so the library can track an item across zones.
What is the difference between the consider and finalize events?
Consider fires continuously while an item is dragged and carries a proposed ordering, while finalize fires once when the drag settles and carries the committed ordering. Both expose the new array under e.detail.items, so committing only on finalize is a matter of which handler you wire up.
How do I stop items being dragged between two lists?
Assign each zone a distinct type so the two zones no longer share a pool, or set dropFromOthersDisabled on the zone that should refuse incoming items. Both options can be changed at runtime and the zone adjusts without a remount.
Why do my placeholder animations look out of sync with the drop?
Set flipDurationMs on the zone to the same number you pass to the flip animation on your items, so the space opening up and the element sliding into it use one duration. Leave it unset and it defaults to 100ms, or set it to zero to turn animation off.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/isaachagoel-svelte-dnd-action)