@thisux/sveltednd Documentation

repository·main·Indexed 20 days ago

https://github.com/thisuxhq/sveltednd

A lightweight, high-performance drag and drop library for Svelte 5 applications, leveraging the runes system for reactive state management. It provides draggable and droppable actions, support for grid and horizontal layouts, keyboard accessibility, and integration with Svelte 5.29+ {@attach} syntax for custom components.

Tokens
20.8K
Snippets
59
Records
86
Agent score
68%

What's inside @thisux/sveltednd

  1. Use Actions vs Attachments

    main

    Choose between standard Svelte actions or the {@attach} factory based on your component structure:

    • use:draggable / use:droppable: Use these for plain HTML elements within your current component.
    • attachDraggable / attachDroppable: Use these for custom Svelte components (requires Svelte 5.29+). The component must spread its props onto a root element (e.g., <div {...props}>).

    Best Practice: When using attachments, pass a getter function to the attachment so that the options (like container or dragData) remain reactive when your data changes.

    <Card
    	{@attach attachDraggable(() => ({
    		container: columnId,
    		dragData: task
    	}))}
    >
    	{task.title}
    </Card>
    
    <Column
    	{@attach attachDroppable(() => ({
    		container: columnId,
    		callbacks: { onDrop: handleDrop }
    	}))}
    >
    	<!-- cards -->
    </Column>
  2. Handle Nested Drop Zones

    main

    When implementing nested zones (a droppable inside another droppable), the deepest matching droppable handles the drop.

    Best Practices:

    • Ensure both parent and child zones have distinct container IDs.
    • Validate the targetContainer inside your onDrop callback to ensure the item is being moved to the intended level.
  3. How @thisux/sveltednd works: Mental Model

    main

    The library follows a data-agnostic approach where you own the state. It provides the interaction layer, but does not mutate your data arrays.

    Core Concepts

    • container: A string ID representing a list, column, or zone. The library compares sourceContainer and targetContainer to determine if a move is valid.
    • dragData: A typed payload (T) carried through the drag operation. It is recommended to use stable objects containing a unique id.
    • dndState: A global reactive snapshot providing real-time information such as isDragging, draggedItem, active containers, dropPosition, and invalidDrop status.
    • Data Ownership: You must implement the actual data reordering or movement logic inside the onDrop callback using the information provided in the DragDropState.
  4. Access global DragDropState

    main

    The dndState is a global reactive object that tracks the current drag-and-drop session. You can use it for overlays, cross-component UI, or conditional logic.

    DragDropState<T> Fields:

    • isDragging: boolean - Is a drag session active?
    • draggedItem: T - The payload from the draggable.
    • sourceContainer: string - The ID of the origin container.
    • targetContainer: string | null - The ID of the hovered/drop container.
    • targetElement: HTMLElement | null - The element currently under the pointer.
    • dropPosition: 'before' | 'after' | null - The relative side for insertion.
    • invalidDrop: boolean? - An app-controlled flag to reject a drop.
    • dragInput: DragInputMode | null - The input method ('html5', 'pointer', or 'keyboard').

    Conditional Drops: To prevent a drop, set dndState.invalidDrop = true inside an onDragOver callback. The library will honor this during the onDrop phase.

    import { dndState } from '@thisux/sveltednd';
    
    // In a component
    $effect(() => {
      if (dndState.isDragging) {
        console.log('Currently dragging:', dndState.draggedItem);
      }
    });
  5. How to use Attachments (`{@attach}`) with components

    main

    In @thisux/sveltednd, Svelte 5.29+ attachments ({@attach}) are used to apply drag-and-drop behavior to child components rather than plain DOM elements.

    When to use Attachments vs Actions

    • Use Actions (use:draggable / use:droppable): When the target is a plain HTML element (e.g., <div>, <li>) within your current file.
    • Use Attachments (attachDraggable / attachDroppable): When the target is a custom component (e.g., <Card>, <Column>).

    Component Contract Requirement

    For attachments to work, the target component must spread its received props onto its root DOM element. If the component does not use {...props} on its root node, the attachment will fail silently because the drag-and-drop logic cannot reach the DOM.

    <!-- Required pattern for target components -->
    <script lang="ts">
    	let { children, ...props }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
    </script>
    
    <div {...props}>
    	{@render children?.()}
    </div>
  6. Handle keyboard drop events in `onDrop`

    main

    Keyboard drops trigger the exact same onDrop callback as mouse or touch interactions. The resulting DragDropState object will have dndState.dragInput === 'keyboard' during the session.

    If you use conditional validation by setting dndState.invalidDrop in your onDragOver callback, the library will automatically announce the invalidity to screen readers and prevent the keyboard drop from committing.

  7. Implement Grid and Horizontal Layouts

    main

    Control the drop direction using the direction property in use:droppable.

    • Grid: Uses nearest-edge detection for grid layouts.
    • Horizontal: Optimized for horizontal lists.
    <!-- Grid Layout -->
    <div class="grid grid-cols-3 gap-4">
    	{#each items as item, index (item.id)}
    		<div
    			use:draggable={{ container: index.toString(), dragData: item }}
    			use:droppable={{
    				container: index.toString(),
    				direction: 'grid',
    				callbacks: { onDrop: handleDrop }
    			}}
    		>
    			{item.name}
    		</div>
    	{/each}
    </div>
    
    <!-- Horizontal Layout -->
    <div class="flex gap-4">
    	{#each items as item, index (item.id)}
    		<div
    			use:draggable={{ container: index.toString(), dragData: item }}
    			use:droppable={{
    				container: index.toString(),
    				direction: 'horizontal',
    				callbacks: { onDrop: handleDrop }
    			}}
    		>
    			{item}
    		</div>
    	{/each}
    </div>
  8. Use {@attach} for Svelte components (Svelte 5.29+)

    main

    Standard Svelte actions (use:draggable, use:droppable) only work on native HTML elements. To apply drag-and-drop functionality to custom Svelte components, use the attachment factories attachDraggable and attachDroppable with the {@attach} syntax.

    Requirements:

    1. Svelte Version: Requires Svelte 5.29 or higher.
    2. Prop Forwarding: Your component must forward all props (including those from the attachment) to a real DOM node using {...props}.
    3. Reactive Options: When passing options that depend on reactive state, you must pass a getter function (e.g., () => ({ ... })) rather than a raw object. This ensures the attachment updates without remounting the component.

    If you are on an older version of Svelte 5, wrap your component in a <div> and use the standard use:draggable or use:droppable actions on that div.

    <script lang="ts">
    	import { attachDraggable, attachDroppable, type DragDropState } from '@thisux/sveltednd';
    	let task = $state({ id: '1', title: 'Ship attach API' });
    
    	function handleDrop(state: DragDropState<any>) {
    		// update your data
    	}
    </script>
    
    <!-- Using attachDraggable on a component -->
    <Card
    	{@attach attachDraggable(() => ({ 
    		container: 'list', 
    		dragData: task 
    	}))}
    >
    	{task.title}
    </Card>
    
    <!-- Using attachDroppable on a component -->
    <Column
    	{@attach attachDroppable(() => ({ 
    		container: 'todo', 
    		callbacks: { onDrop: handleDrop } 
    	}))}
    >
    	<!-- cards -->
    </Column>
  9. Access global drag state for UI overlays

    main

    The dndState object provides global information about the current drag operation, which can be used to build overlays, ghost UIs, or accessibility announcements.

    Commonly used properties:

    • dndState.isDragging: Boolean indicating if a drag operation is active.
    • dndState.sourceContainer: The ID of the container where the drag started.
    • dndState.invalidDrop: Boolean indicating if the current hover target is invalid.

    Example Usage:

    <script>
    	import { dndState } from '@thisux/sveltednd';
    </script>
    
    {#if dndState.isDragging}
    	<p class="sr-only">Dragging from {dndState.sourceContainer}</p>
    {/if}
    #!svelte
    <script>
    	import { dndState } from '@thisux/sveltednd';
    </script>
    
    {#if dndState.isDragging}
    	<p class="sr-only">Dragging from {dndState.sourceContainer}</p>
    {/if}
  10. Common DnD Recipes

    main

    Drag Handle

    Restrict dragging to a specific element within a card using a CSS selector:

    use:draggable={{ container: 'list', dragData: item, handle: '.drag-handle' }}

    Conditional Drop

    Reject drops based on application logic by using onDragOver and dndState.invalidDrop:

    1. In onDragOver, set dndState.invalidDrop = true to reject the current hover.
    2. In onDrop, check the invalidDrop status.
    3. Clear the status on onDragEnd.

    Keyboard Reordering

    Enable accessibility by adding keyboard: true.

    • Workflow: Tab to item $\rightarrow$ Space/Enter to grab $\rightarrow$ Arrows to move preview $\rightarrow$ Space/Enter to drop $\rightarrow$ Escape to cancel.

    Layout Direction

    Set the drop orientation for grids or galleries:

    use:droppable={{ container: 'gallery', direction: 'horizontal', callbacks: { onDrop } }}
    use:droppable={{ container: index.toString(), direction: 'grid', callbacks: { onDrop } }}
  11. Enable keyboard accessibility for draggable items

    main

    You can enable keyboard-based reordering by setting keyboard: true in the draggable action options. This allows users to interact with the drag-and-drop interface using a keyboard instead of a mouse or touch.

    Keyboard Controls:

    • Tab: Focus a keyboard-enabled item.
    • Space / Enter: Pick up an item, or drop it while dragging.
    • ↑ / ↓ (or ← / → for horizontal lists): Move the drop preview among registered drop zones.
    • Escape: Cancel the operation without triggering the onDrop callback.

    Keyboard reordering uses the same onDrop contract as pointer/HTML5 interactions, ensuring your data model logic remains consistent.

    <div
    	use:draggable={{
    		container: 'container-id',
    		dragData: item,
    		keyboard: true
    	}}
    >...</div>
  12. Forward props in components for attachments

    main

    To ensure attachDraggable and attachDroppable work correctly on custom components, the component must spread its props onto a root DOM element. This allows the attachment logic to bind to the actual element in the DOM.

    Example Component Pattern:

    <script lang="ts">
    	let { children, ...props } = $props();
    </script>
    
    <div {...props}>
    	{@render children?.()}
    </div>
    <!-- Card.svelte -->
    <script lang="ts">
    	let { children, ...props } = $props();
    </script>
    
    <div {...props}>
    	{@render children?.()}
    </div>