# @thisux/sveltednd
> @thisux/sveltednd is a lightweight, TypeScript-first drag and drop library for Svelte 5. It provides two Svelte actions (`draggable`, `droppable`) and a global reactive state object (`dndState`) built on Svelte 5 runes. The library uses a dual-API approach — HTML5 Drag-and-Drop for desktop and Pointer Events for touch/mobile — giving seamless cross-device support with no extra configuration.
Open source by [THISUX Private Limited](https://thisux.com), a design-led product studio. Created by [Sanju](https://sanju.sh).
Install via npm: `npm install @thisux/sveltednd`
NPM package: https://www.npmjs.com/package/@thisux/sveltednd
Demo site: https://sveltednd.thisux.com
GitHub: https://github.com/thisuxhq/sveltednd
Studio: https://thisux.com
Contact: hello@thisux.com
## Demo Pages
- [Kanban Board](/): Multi-column drag and drop — move tasks between todo, in-progress, and done columns
- [Simple List](/simple-list): Vertical sortable list — reorder items by dragging
- [Grid Sort](/grid-sort): 2D grid reordering with nearest-edge drop detection
- [Horizontal Scroll](/horizontal-scroll): Horizontal list drag with left/right drop indicators
- [Drag Handle](/drag-handle): Restrict dragging to a specific handle element (grip icon pattern)
- [Nested Containers](/nested): Drag between nested drop zones
- [Multiple Lists](/multiple): Several independent draggable lists on the same page
- [Interactive Elements](/interactive-elements): Draggable items containing buttons, inputs, and links that remain fully interactive
- [Conditional Check](/conditional-check): Accept or reject drops based on custom logic
- [Custom Classes](/custom-classes): Override default drag/drop CSS classes with your own
- [Attachments](/attach): `{@attach attachDraggable}` / `attachDroppable` on components
- [Keyboard](/keyboard): Opt-in Space / arrows / Escape reordering with live announcements
## Core Exports
From `@thisux/sveltednd`:
- `draggable` — Svelte action. Attach to any element to make it draggable.
- `droppable` — Svelte action. Attach to any element to make it a drop target.
- `attachDraggable` / `attachDroppable` — Attachment factories for `{@attach}` (Svelte 5.29+).
- `dndState` — Global reactive state object. Tracks the active drag operation in real time.
- `resetDndState` — Force-clear global drag state.
## draggable Action
Makes any HTML element draggable. Uses HTML5 Drag API on desktop and Pointer Events on touch devices. Automatically sets `touch-action: none` and `user-select: none` inline for mobile compatibility.
```svelte
{item.title}
```
### DraggableOptions\
All options from `DragDropOptions` plus:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `container` | `string` | Yes | Unique identifier for this drag group. Used to track where drags originate. |
| `dragData` | `T` | No | The data payload attached to this element. Available in all callbacks as `state.draggedItem`. |
| `disabled` | `boolean` | No | When true, disables all drag functionality on this element. |
| `handle` | `string` | No | CSS selector for a drag handle. When set, dragging only starts from that element (e.g. `'.drag-handle'`, `'[data-drag-handle]'`). |
| `interactive` | `string[]` | No | Extra CSS selectors for child elements that should remain clickable and not trigger drag. By default: `input`, `textarea`, `select`, `button`, `[contenteditable]`, `a[href]`, `label`, `option`. |
| `callbacks` | `DragDropCallbacks` | No | Lifecycle event handlers. |
| `attributes` | `DragDropAttributes` | No | Custom CSS class overrides. |
| `direction` | `'vertical' \| 'horizontal' \| 'grid'` | No | List layout. Defaults to `'vertical'`. Affects drop indicator direction and position detection. |
| `keyboard` | `boolean \| KeyboardOptions` | No | Opt-in keyboard reordering (`true` enables Space/Enter grab & drop, arrows to move, Escape to cancel). Default off. |
### Keyboard example
```svelte
{item.title}
```
Keys: Tab focus → Space/Enter pick up → arrows move preview → Space/Enter drop → Escape cancel.
Uses the same `onDrop` contract as pointer/HTML5. Assertive live region announces status.
### Handle Example
```svelte
⠿{item.title}
```
### Interactive Elements Example
```svelte
{item.title}
```
## droppable Action
Makes any HTML element a drop target. Handles both HTML5 drag events and custom pointer events. Uses an enter counter internally to correctly handle nested child elements without spurious dragleave events.
```svelte
{#each items as item}
{item}
{/each}
```
### DragDropOptions\ (used by droppable)
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `container` | `string` | Yes | Identifier for this drop zone. Matches against `dndState.sourceContainer`. |
| `dragData` | `T` | No | Data payload (usually set on draggable, not droppable). |
| `disabled` | `boolean` | No | When true, this element won't accept drops. |
| `callbacks` | `DragDropCallbacks` | No | Lifecycle event handlers. |
| `attributes` | `DragDropAttributes` | No | Custom CSS class overrides. |
| `direction` | `'vertical' \| 'horizontal' \| 'grid'` | No | Controls drop indicator direction. `'vertical'` shows horizontal lines above/below; `'horizontal'` shows vertical lines left/right; `'grid'` uses nearest-edge detection on all four sides. |
## dndState
A global reactive object (Svelte 5 `$state` rune) that tracks the current drag operation. Any component can import and reactively read from it. Updated automatically by `draggable` and `droppable` — do not mutate it manually.
```svelte
{#if dndState.isDragging}
Dragging {dndState.draggedItem?.title} from {dndState.sourceContainer}
{/if}
```
### DragDropState\ fields
| Field | Type | Description |
|-------|------|-------------|
| `isDragging` | `boolean` | True from dragstart until dragend. |
| `draggedItem` | `T \| null` | The data payload being dragged. Null when not dragging. |
| `sourceContainer` | `string` | Container ID where the drag started. Empty string when not dragging. |
| `targetContainer` | `string \| null` | Container ID currently being hovered over. Updates in real time. Null if not over a valid drop zone. |
| `targetElement` | `HTMLElement \| null` | The specific DOM element under the cursor. Useful for precise per-item highlighting. |
| `dropPosition` | `'before' \| 'after' \| null` | Where the item will land relative to `targetElement`. `'before'` = above/left, `'after'` = below/right. Null when not over an item. |
| `invalidDrop` | `boolean` | Set to true when hovering over an invalid drop zone. Useful for red-highlighting feedback. |
## Callbacks (DragDropCallbacks\)
All callbacks receive the current `DragDropState`. Lifecycle order for a complete drag:
1. `onDragStart` — user began dragging
2. `onDragEnter` — entered a valid drop zone
3. `onDragOver` — moving within a drop zone (fires on every pointer move — keep it lightweight)
4. `onDrop` — item released over the drop zone (async supported)
5. `onDragEnd` — drag ended, fires after `onDrop` regardless of success or cancellation
6. `onDragLeave` — left a drop zone (fires if they moved to another zone before dropping)
```typescript
const callbacks: DragDropCallbacks = {
onDragStart: (state) => console.log('started', state.draggedItem),
onDragEnter: (state) => highlight(state.targetContainer),
onDragLeave: (state) => unhighlight(state.targetContainer),
onDragOver: (state) => updateIndicator(state.dropPosition),
onDrop: async (state) => {
await moveItem(state.draggedItem, state.targetContainer, state.dropPosition);
},
onDragEnd: (state) => cleanup()
};
```
## CSS Classes (DragDropAttributes)
Override the default CSS classes applied during drag:
```svelte
dragOverClass: 'border-blue-500 bg-blue-50'
}
}}
>
```
| Attribute | Default | When applied |
|-----------|---------|--------------|
| `draggingClass` | `'dragging'` | Added to the element being dragged |
| `dragOverClass` | `'drag-over'` | Added to the drop zone being hovered |
Multiple classes are supported, space-separated.
## CSS Stylesheet
Import the default styles (required for drop position indicators):
```typescript
import '@thisux/sveltednd/dnd.css';
// or
import '$lib/styles/dnd.css';
```
### CSS Classes Reference
| Class | Applied when |
|-------|-------------|
| `.dragging` | Element is actively being dragged (default, override with `draggingClass`) |
| `.drag-over` | Drop zone is being hovered (default, override with `dragOverClass`) |
| `.svelte-dnd-draggable` | Base class for draggable elements (`touch-action: none`, `user-select: none`) |
| `.svelte-dnd-touch-feedback` | Alias for touch support on inner elements without `use:draggable` |
| `.svelte-dnd-dragging` | While dragging: `opacity: 0.5`, `cursor: grabbing` |
| `.svelte-dnd-droppable` | Base class for drop zones (`position: relative`) |
| `.svelte-dnd-drop-target` | Valid drop zone being hovered: `outline: 2px dashed #4caf50` |
| `.svelte-dnd-invalid-target` | Invalid drop zone: `outline: 2px dashed #f44336` |
| `.svelte-dnd-placeholder` | Ghost placeholder: `border: 2px dashed #9e9e9e` |
| `.drop-before` | Drop indicator line drawn above the element (2px blue `::before` pseudo-element) |
| `.drop-after` | Drop indicator line drawn below the element (2px blue `::after` pseudo-element) |
| `.drop-left` | Drop indicator line drawn to the left (horizontal lists) |
| `.drop-right` | Drop indicator line drawn to the right (horizontal lists) |
## Complete Examples
### Sortable List
```svelte
{#each items as item (item)}
{/if}
```
## Using with Components — attachments (Svelte 5.29+)
Svelte actions only work on native HTML elements. For components, use first-class
attachment factories: `attachDraggable` and `attachDroppable`.
```svelte
({ container: 'list', dragData: task }))}>
{task.title}
(() => ({
container: 'todo',
callbacks: { onDrop: handleDrop }
}))}>
```
The receiving component must spread props onto a root element:
```svelte
{@render children?.()}
```
Prefer a getter `() => options` so reactive state is read on each attach evaluation.
If you use `fromAction` yourself, the second argument must be a getter:
`fromAction(draggable, () => options)` — not the raw options object.
Requires Svelte 5.29+. On older versions, wrap the component in a `
` with `use:`.
## Architecture
The library is built on three files:
- `src/lib/actions/draggable.ts` — Dual-mode drag: HTML5 `dragstart`/`dragend` + `pointerdown`/`pointermove`/`pointerup`. Tracks the actual clicked element for correct interactive-child detection. On pointerup, uses `document.elementFromPoint` to find the drop target and dispatches a custom `pointerdrop-on-container` event. Handles `pointercancel` for mobile gesture cancellation (scroll takeover, palm rejection) using a dedicated handler that checks `html5DragActive` — so desktop HTML5 drag (which also fires `pointercancel` when the browser takes over) is correctly ignored.
- `src/lib/actions/droppable.ts` — Drop zone implementation. Uses a `dragEnterCounter` to correctly handle nested element bubbling. For drop indicators, uses a sibling-transfer strategy (vertical/horizontal) and nearest-edge normalised detection (grid). Listens for both native drag events and the custom `pointerdrop-on-container` event. Also listens at `document` level for `dragend` to clean up orphaned hover classes and for `pointermove` to track hover position using `getBoundingClientRect` — this is required for touch/mobile where `pointerover`/`pointerout` do not fire on elements beneath the finger. A `wasOver` boolean per droppable fires `onDragEnter`/`onDragLeave` only on transitions.
- `src/lib/stores/dnd.svelte.ts` — 11-line file. Just a `$state` object. Global so droppables don't need prop drilling.
### Touch/Mobile Notes
- The `draggable` action sets `touch-action: none` and `user-select: none` inline on the element. This is critical — without `touch-action: none`, touch gestures trigger scroll instead of pointer events, breaking mobile drag entirely.
- Pointer capture (`setPointerCapture`) is intentionally NOT used. Using it would redirect all pointer events to the dragged element and prevent coordinate-based hover detection from working correctly. Instead, document-level `pointermove`/`pointerup` listeners track the drag.
- On touch devices, `pointerover`/`pointerout` do not fire on elements beneath the finger during a drag — only on the element where the touch started. The droppable uses document-level `pointermove` + `getBoundingClientRect` to detect hover by coordinates instead, which works on both mouse and touch.
- On desktop, the browser fires `pointercancel` immediately after `dragstart` to release the pointer for the HTML5 drag. The `draggable` action guards against this with an `html5DragActive` flag so the pointer-cancel path does not reset state mid-drag.
## TypeScript
The library is written in strict TypeScript. All types are exported:
```typescript
import type {
DragDropState, // State object shape
DragDropCallbacks, // Callback function signatures
DragDropAttributes, // CSS class overrides
DragDropOptions, // Base options (used by droppable)
DraggableOptions, // Extended options (used by draggable, adds handle + interactive)
} from '@thisux/sveltednd';
```
All generic types use `T = unknown` as default, so they work without specifying a type parameter when you don't need strict typing.
## Package Info
- Package name: `@thisux/sveltednd`
- Version: 0.1.2
- License: MIT
- Peer dependency: `svelte@^5.0.0`
- Module type: ESM only (`"type": "module"`)
- Entry point: `./dist/index.js`
- Types: `./dist/index.d.ts`
- Svelte entry: `./dist/index.js`
## Design System
SvelteDnD includes a Swiss Grid design system demo with:
- **Dark mode support** — System preference detection with manual toggle
- **Swiss Grid aesthetic** — Black/white/red color palette with geometric elements
- **Clean typography** — Google Sans font with consistent 500 weight
- **Generous whitespace** — Border-based layouts with minimal styling
The demo website showcases these design principles across all 10 example pages.