# @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)}
{item}
{/each} ``` ### Kanban Board (Multi-Column) ```svelte {#each columns as column}

{column}

{#each tasks.filter(t => t.status === column) as task (task.id)}
{task.title}
{/each}
{/each} ``` ### Drag Handle ```svelte {#each items as item (item.id)}
{item.title}
{/each} ``` ### Grid Sort ```svelte
{#each cells as cell (cell)}
{cell}
{/each}
``` ### Reading dndState for Real-Time Feedback ```svelte {#if dndState.isDragging}
Moving: {dndState.draggedItem?.title} → {dndState.targetContainer ?? 'none'}
{/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.