Guide
EffDND adds drag & drop to your HTML with just attributes. No JavaScript calls, no setup beyond a single import. This guide explains what you can do with it and how.
Getting started
npm i effdndImport once in your app:
import 'effdnd'; // the library
import 'effdnd/style.css'; // optional: default stylesThat's it. Any element marked with the attributes below becomes draggable immediately — even elements added to the page later.
Frameworks: works with any of them. No component wrapper is needed.
How it works
EffDND is built around one core idea, and everything else in this guide is just an extension of it:
- Only a
triggerstarts a drag. Nothing is draggable on its own. You grab a handle marked withdata-dnd-triggerand the drag begins; without one, nothing ever moves. - The
itemis what actually moves. The trigger usually lives inside the item so the whole element follows your pointer, but they do not have to be the same node. - The
scopeis the sandbox. Theitemcan only move and be dropped within itsscope. Drop zones or containers outside it are simply ignored.
That is the base case: a trigger on an item inside a scope.
<div data-dnd-scope="board"> <!-- scope: the sandbox -->
<div data-dnd-item="a"> <!-- item: what moves -->
<span data-dnd-trigger>⠿</span> <!-- trigger: what you grab -->
Grab me
</div>
</div>Every feature that follows — reorder, transfer, scrolling, or the trigger parameters below — only refines or extends this core behavior.
Styling while dragging. The element you see moving is a clone (a copy) of the
item, inserted into the item's own parent. This means cascaded and inherited styles, and rules written against parent selectors (ul > li,.zone .card, …), still apply to the moving clone — no need to inline every color. The original item stays in place in the list and is dimmed via thepassivestate (see Styling below).Caveat — transformed ancestors. The clone is
position:fixed, so it is detached from the layout. But if any ancestor of the item hastransform,filter,perspective(orwill-change:transform), the browser treatsfixedlikeabsolute: the clone is then positioned relative to that ancestor and may appear offset (and it will scroll with a scrollable transformed container). Avoid transformed ancestors of dragged items, or override the fixed-visual styles viadata-dnd-stateand the inline styles you place on the item.
The attributes at a glance
| Attribute | What it does |
|---|---|
data-dnd-trigger | Marks the "handle" the user drags from |
data-dnd-item | Marks the element that gets dragged |
data-dnd-scope | Limits drags and drop targets to one container |
data-dnd-reorder | Turns a container into a sortable list (x or y) |
data-dnd-target | Marks a drop zone |
data-dnd-transfer | Gives a drop zone an action: append, prepend, or remove |
data-dnd-scroll | Auto-scrolls the container while dragging |
data-dnd-transition | Smooth-out the moving animation |
data-dnd-disabled | Disables a trigger |
data-dnd-state | Runtime state (active/passive) set during a drag — used for styling |
Only three of them are required for the most basic case: a handle (trigger) on an element (item) inside a container (scope). Everything else is optional and adds behavior.
Trigger parameters
Because the trigger is the entry point, it is the attribute you tune most often. It accepts a semicolon-separated list of parameters:
<span data-dnd-trigger="dist:12;axis:y;scope:board;item:task-1">⠿</span>Require a drag distance (dist)
Prevents accidental drags on a click — only drags after the pointer moves the given number of pixels:
<span data-dnd-trigger="dist:12">⠿</span> <!-- only drags after the pointer moves 12px -->Lock to one axis (axis)
<span data-dnd-trigger="axis:x">⠿</span> <!-- horizontal only -->
<span data-dnd-trigger="axis:y">⠿</span> <!-- vertical only -->Address a specific container (scope)
By default EffDND finds the nearest scope by looking at the DOM. When you need a specific one, name it:
<span data-dnd-trigger="scope:board">⠿</span>Address a specific item (item)
The trigger can specify which specific item above it to move:
<span data-dnd-trigger="scope:board;item:task-1">⠿</span>Restrict drop targets (target)
Only allow dropping onto zones whose data-dnd-target starts with the given name:
<span data-dnd-trigger="scope:board;target:drop">⠿</span>Move freely across the whole page (scope:*)
The special scope:* value ignores any scope boundary entirely:
<span data-dnd-trigger="scope:*">⠿</span>The item can then be dragged anywhere on the page.
Disable a trigger
<span data-dnd-trigger data-dnd-disabled>⠿</span>Remove the attribute to re-enable.
Typical scenarios
These are the common ways the pieces come together in real interfaces.
Reorder: a sortable list
Wrap your items in a list with data-dnd-reorder="y", mark each item, and add a handle.
<ul data-dnd-scope="todo" data-dnd-reorder="y">
<li data-dnd-item="1"><span data-dnd-trigger>⠿</span>Buy milk</li>
<li data-dnd-item="2"><span data-dnd-trigger>⠿</span>Read a book</li>
</ul>ysorts vertically,xsorts horizontally.- Only items that are direct children of the list are rearranged.
Transfer: move items between containers
Mark a container as a drop zone and give it a transfer action.
<div data-dnd-scope="sprint">
<div class="zone" data-dnd-target="backlog" data-dnd-transfer="append">Backlog</div>
<div class="zone" data-dnd-target="done" data-dnd-transfer="prepend">Done</div>
</div>append— the item is added to the end of the zone.prepend— the item is added to the start of the zone.remove— the item is deleted (great for a trash bin).
The drop zones must live inside the same scope (or use scope:*, see above).
Combine: a sortable list with a trash bin
<div data-dnd-scope="mailbox">
<ul data-dnd-reorder="y">
<li data-dnd-item="1"><span data-dnd-trigger>⠿</span>Invoice</li>
<li data-dnd-item="2"><span data-dnd-trigger>⠿</span>Newsletter</li>
</ul>
<button data-dnd-target="trash" data-dnd-transfer="remove">Delete</button>
</div>Here the list sorts itself and rows can be thrown away by dropping them on the button.
Kanban: columns and cards
Give the board data-dnd-reorder="x" to sort columns, and each column's card area both a vertical reorder zone and a transfer receiver.
<div data-dnd-scope="kanban" data-dnd-reorder="x"> <!-- sorts columns -->
<div class="col" data-dnd-item="col-1">
<div class="col-head"><span data-dnd-trigger>⠿</span>Backlog</div>
<div class="cards" data-dnd-reorder="y"
data-dnd-target="col-1" data-dnd-transfer="append"> <!-- sorts & accepts cards -->
<div class="card" data-dnd-item="task-1"><span data-dnd-trigger>⠿</span>Write spec</div>
</div>
</div>
<!-- more columns ... -->
</div>Drag a column header to reorder columns; drag a card to move it between columns.
Tuning common behaviors
Auto-scroll a long container
Attach data-dnd-scroll to the scrollable element:
<div data-dnd-scroll="threshold:50;speed:12">threshold(default30) — how close to an edge scrolling starts, in px.speed(default10) — how fast it scrolls, in px per frame.
Dragging near an edge now scrolls the container, and scrolling accelerates the closer to the edge you get.
Smooth the animation
<div data-dnd-transition="150ms ease">…</div>Default is 100ms linear.
Reacting to drags from JavaScript
You still can — EffDND just makes it optional. Each function below returns an unsubscribe function:
import { onDrag, onDrop, onReorder, onTransfer, onDragStart, onDragEnd } from 'effdnd';
onReorder((event) => {
console.log('Item was reordered:', event.detail.keys.item);
});Available events: effdragstart, effdrag, effdragend, effdragenter, effdragleave, effdrop, effreorder, efftransfer.
Every event gives you a detail object you can rely on:
event.detail.keys.item; // which item
event.detail.keys.scope; // in which scope
event.detail.keys.target; // on which target
event.detail.item;Helpers you may find useful
import { getItem, getScope, getTargets, getReorderContainer, reset } from 'effdnd';
getItem(trigger); // the item an element belongs to
getScope(trigger); // the scope an element belongs to
getTargets(trigger); // the drop zones available for a trigger
reset(item); // snap an item back to its original positionStyling with data-dnd-state
During a drag EffDND tags the involved elements with a runtime data-dnd-state attribute. This gives you a clean hook to style the active drag — no inline JavaScript needed. No data-dnd-state is present when nothing is being dragged.
State values
| Element | State | Meaning |
|---|---|---|
data-dnd-item | active | The moving clone — the element currently following your pointer |
data-dnd-item | passive | The original item left in place, dimmed behind the clone |
data-dnd-scope | active | The pointer is outside the scope's boundary |
data-dnd-scope | passive | The pointer is inside the scope — normal while dragging |
data-dnd-target | active | The drop zone currently being hovered |
data-dnd-target | passive | A valid drop zone that is ready to accept a drop |
The bundled index.css already ships tasteful defaults built on these selectors, and you use the very same selectors to override them:
/* Defaults from index.css */
[data-dnd-item][data-dnd-state="passive"] { opacity: 0.2; }
[data-dnd-item][data-dnd-state="active"] { opacity: 0.75; z-index: 1000; }
/* Your own theme */
[data-dnd-item][data-dnd-state="passive"] { opacity: 0.08; }
[data-dnd-item][data-dnd-state="active"] {
opacity: 1;
box-shadow: 0 0 0 2px cornflowerblue;
border-radius: 8px;
}
[data-dnd-scope][data-dnd-state="active"] { outline: 2px dashed tomato; }
[data-dnd-target][data-dnd-state="active"] { background: rgba(100, 200, 255, 0.2); }Because the defaults live in index.css, loading them is optional: if you skip that import and write your own [data-dnd-state="…"] rules, EffDND stays fully dependency-free and you stay in complete visual control.
Tip. The original is dimmed by
opacity, not removed — so the list keeps its layout and siblings don't jump while you drag. If you want the original to disappear entirely, setopacity: 0(orvisibility: hidden) for thepassivestate.
Mini reference
A condensed version of every attribute and its parameters.
| Attribute / parameter | Values | Default | Purpose |
|---|---|---|---|
data-dnd-trigger | — | — | Marks the handle that starts a drag |
… dist | number | — | Drag only after the pointer moves this many px |
… axis | x / y | both | Lock the drag to one axis |
… scope | name / * | nearest in DOM | Use a specific container (or ignore scopes) |
… item | name | nearest in DOM | Drag a specific item |
… target | name | all | Only allow drops on targets matching a name |
data-dnd-disabled | — | — | Disables a trigger |
data-dnd-item | unique name | — | Marks the element that gets dragged |
data-dnd-scope | unique name | — | Limits drags/drops to one container |
data-dnd-reorder | x / y | — | Turns a container into a sortable list |
data-dnd-target | name | — | Marks a drop zone |
data-dnd-transfer | append / prepend / remove | — | Action taken when dropped on a zone |
data-dnd-scroll | threshold;speed | 30;10 | Auto-scrolls the container while dragging |
data-dnd-transition | duration & easing | 100ms linear | Smooths the moving animation |
data-dnd-state | active / passive | — | Runtime drag state, styled via CSS (see Styling) |