Tiny & Memory-optimized Drag and Drop Library – snap-dnd

Tiny & Memory-optimized Drag and Drop Library – snap-dnd
Tiny & Memory-optimized Drag and Drop Library – snap-dnd
snap-dnd is a lightweight JavaScript library that provides smooth, high-performance drag and drop interactions for modern web applications.

The library works natively with Web Components, Shadow DOM, and Lit Elements through proper event delegation and scoped querying.

You can implement drag-drop functionality using either declarative data attributes or a full imperative API.

Features:

  • Memory Optimized: Uses object pooling for coordinate and rectangle calculations. WeakMap caches prevent memory leaks. Event delegation reduces listener overhead.
  • Web Component Compatible: Works inside Shadow DOM boundaries. Properly handles composed event paths. Supports Lit Element lifecycle methods.
  • Touch Device Support: Handles mouse, touch, and pointer events through a unified API.
  • Modular Plugin System: Core library handles basic drag-drop. Optional plugins add Sortable, Kanban, and FileDrop functionality.
  • Declarative and Imperative APIs: Start with simple data attributes for prototypes. Switch to the full JavaScript API for complex implementations.
  • Performance Features: Grid snapping with configurable thresholds. Axis constraints for horizontal or vertical-only movement. Auto-scroll near viewport edges.
  • Framework Agnostic: No React, Vue, or Angular dependencies. Works with any frontend stack or plain HTML.

See It In Action:

Use Cases:

  • Task Management Boards: Build Trello-style kanban boards with drag-drop between columns. The Kanban plugin handles multi-container movement with visual feedback and insertion index tracking.
  • Sortable Lists and Grids: Create reorderable todo lists, media galleries, or dashboard widgets. The Sortable plugin provides smooth animations and placeholder elements during drag operations.
  • File Upload Interfaces: Accept file drops from desktop with type validation and size limits. The FileDrop plugin handles drag-over states and file extraction from DataTransfer objects.
  • Form Builders and Visual Editors: Enable you to construct forms or page layouts by dragging components. Axis constraints and grid snapping ensure precise positioning.

How To Use It:

1. Install snap-dnd with NPM and import it into your project.

# NPM
$ npm install snap-dnd
// Core only - approximately 5KB gzipped
import { Snap } from 'snap-dnd/core';

// Full bundle with plugins - approximately 9KB gzipped
import { Snap, Sortable, Kanban, FileDrop } from 'snap-dnd';
// For Browser
import { Snap, Sortable, Kanban, FileDrop } from 'snap.esm.js';

2. Implement snap-dnd using declarative HTML attributes. Add data-draggable to items and data-droppable to containers. Snap detects these automatically.

  • data-draggable: Marks an element as draggable. No value required.
  • data-droppable: Marks an element as a drop zone. No value required.
  • data-drag-handle: Restricts drag initiation to a specific child element. Use a selector value.
  • data-drag-axis="x|y": Constrains movement to horizontal or vertical only.
  • data-drag-id="...": Custom identifier passed to event callbacks.
  • data-drag-type="...": Type string used for drop zone filtering.
  • data-accepts="a,b,c": Comma-separated list of types this drop zone accepts.
  • data-file-drop: Enables file drop handling on this element.
<div id="container">
  <!-- data-draggable makes the element draggable -->
  <div data-draggable>Drag me!</div>
  <div data-draggable>Drag me too!</div>
  <!-- data-droppable creates a drop target zone -->
  <div data-droppable>Drop here</div>
</div>
// Initialize Snap on the container element
const snap = new Snap(document.getElementById('container'), {
  // onDrop fires when an element is released over a drop zone
  onDrop: (e) => {
    console.log('Element:', e.element);
    console.log('Drop zone:', e.dropZone);
    console.log('Insertion index:', e.insertionIndex);
  }
});

3. The library does not inject CSS automatically. Add these styles to your stylesheet:

/* Required for touch device support */
[data-draggable] {
  touch-action: none;
  user-select: none;
  cursor: grab;
}

/* Visual feedback during drag */
.snap-dragging {
  opacity: 0.5;
  cursor: grabbing;
}

/* Drop zone active state */
.snap-drop-active {
  background: rgba(0, 120, 255, 0.1);
  border: 2px dashed rgba(0, 120, 255, 0.5);
}

/* Drag ghost styling */
.snap-ghost {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform-origin: top left;
}

/* Sortable placeholder */
.sortable-placeholder {
  background: rgba(0, 0, 0, 0.05);
  border: 2px dashed #ccc;
  border-radius: 4px;
}

/* File drop hover state */
.snap-file-drag-over {
  background: rgba(0, 120, 255, 0.05);
  border-color: rgba(0, 120, 255, 0.8);
}

4. For complex scenarios, use the imperative API to add elements programmatically:

const container = document.getElementById('board');

const snap = new Snap(container, {
  // onDragStart fires when drag operation begins
  onDragStart: (e) => {
    console.log('Started dragging:', e.element);
    e.element.style.opacity = '0.5';
  },
  
  // onDragMove fires continuously during drag
  onDragMove: (e) => {
    console.log('Current position:', e.position);
    console.log('Delta from start:', e.delta);
  },
  
  // onDrop fires on successful drop
  onDrop: (e) => {
    console.log('Dropped at index:', e.insertionIndex);
    e.element.style.opacity = '1';
  }
});

// Add a draggable element with custom data and constraints
const taskElement = document.querySelector('.task');
snap.addDraggable(taskElement, {
  // data object is accessible in all event callbacks
  data: { id: 1, type: 'task', priority: 'high' },
  // axis restricts movement to one direction
  axis: 'y'
});

// Add a drop zone with type filtering
const columnElement = document.querySelector('.column');
snap.addDropZone(columnElement, {
  // accepts array filters which draggable types can drop here
  accepts: ['task'],
  // onEnter fires when a draggable enters this zone
  onEnter: () => {
    columnElement.classList.add('highlight');
  }
});

// Clean up when component unmounts
snap.destroy();

5. The Snap constructor accepts an options object with the following properties:

  • draggableSelector (string): CSS selector for draggable elements. Defaults to [data-draggable].
  • dropZoneSelector (string): CSS selector for drop zones. Defaults to [data-droppable].
  • handleSelector (string): CSS selector for drag handles. If set, only handles can initiate drag operations.
  • axis (string): Movement constraint. Values: 'x', 'y', or 'both'. Defaults to 'both'.
  • grid (object): Grid snapping configuration. Format: { x: 20, y: 20 }. Coordinates snap to nearest grid point.
  • delay (number): Milliseconds to wait before drag starts. Prevents accidental drags on click.
  • distance (number): Pixels the pointer must move before drag starts. Prevents accidental drags on small movements.
  • autoScroll (boolean or object): Auto-scroll when dragging near viewport edges. Set to true or configure with { threshold: 40, maxSpeed: 15 }.
  • autoRefresh (boolean): Automatically detect DOM changes and refresh internal caches. Useful with frameworks that re-render frequently.
  • ghostClass (string): CSS class applied to the original element during drag. Use for custom styling.
  • renderGhost (function): Custom function to render the drag ghost. Receives the dragged element, returns a new element to use as ghost.
  • throttle (boolean): Enable requestAnimationFrame throttling for drag move events. Defaults to true.
  • onDragStart (function): Callback when drag starts. Receives event object with element, position, and data properties.
  • onDragMove (function): Callback during drag movement. Receives event object with element, position, delta, and dropZone properties.
  • onDragEnd (function): Callback when drag ends. Receives event object with element, position, delta, and cancelled properties.
  • onDrop (function): Callback on successful drop. Receives event object with element, dropZone, position, data, and insertionIndex properties.
  • onDropZoneEnter (function): Callback when draggable enters a drop zone. Receives event object with element, dropZone, and position properties.
  • onDropZoneLeave (function): Callback when draggable leaves a drop zone. Receives event object with element and dropZone properties.

6. API methods.

// Enable drag-drop interactions
snap.enable();

// Disable drag-drop interactions
snap.disable();

// Destroy the instance and clean up all listeners
snap.destroy();

// Refresh internal element caches after DOM changes
snap.refresh();

// Add a draggable element programmatically
snap.addDraggable(element, {
  data: { id: 1 },
  axis: 'y'
});

// Remove a draggable element
snap.removeDraggable(element);

// Add a drop zone programmatically
snap.addDropZone(element, {
  accepts: ['task'],
  onEnter: () => {}
});

// Remove a drop zone
snap.removeDropZone(element);

// Check if currently dragging
const dragging = snap.isDragging();

// Get the currently dragged element
const element = snap.getActiveElement();

// Register a plugin
snap.use(new Sortable());

// Add a behavior
// See below
snap.addBehavior(new AutoScroll());

// Update configuration options
snap.setOptions({
  axis: 'x',
  grid: { x: 10, y: 10 }
});

7. Event handlers.

// Subscribe to drag start events
snap.on('dragstart', (e) => {
  console.log('Drag started:', e.element);
});

// Subscribe to drag move events
snap.on('dragmove', (e) => {
  console.log('Position:', e.position);
});

// Subscribe to drag end events
snap.on('dragend', (e) => {
  console.log('Drag ended:', e.element);
});

// Subscribe to drop events
snap.on('drop', (e) => {
  console.log('Dropped:', e.element, 'into', e.dropZone);
});

// Subscribe to drop zone enter events
snap.on('dropzoneenter', (e) => {
  console.log('Entered zone:', e.dropZone);
});

// Subscribe to drop zone leave events
snap.on('dropzoneleave', (e) => {
  console.log('Left zone:', e.dropZone);
});

// Unsubscribe from specific event
snap.off('dragstart', callbackFunction);

// Unsubscribe from all events of a type
snap.off('dragstart');

8. Use the Sortable plugin:

const container = document.getElementById('list');

// Initialize Snap with Sortable plugin
const snap = new Snap(container).use(new Sortable({
  // animation duration in milliseconds for reordering transitions
  animation: 150,
  // CSS class applied to dragged element
  ghostClass: 'sortable-ghost',
  // CSS class applied to placeholder element
  placeholderClass: 'sortable-placeholder'
}));

// Listen for sort completions
snap.on('drop', (e) => {
  console.log('Moved from index', e.sourceContainer, 'to index', e.insertionIndex);
});

9. Use the Kanban Plugin:

const board = document.getElementById('kanban-board');

// Initialize Snap with Kanban plugin
const snap = new Snap(board).use(new Kanban({
  // CSS selector for container elements
  containers: '.column',
  // CSS selector for draggable items
  items: '.card',
  // animation duration for item movements
  animation: 150
}));

// Handle drops between containers
snap.on('drop', (e) => {
  const movedFrom = e.sourceContainer;
  const movedTo = e.dropZone;
  const newIndex = e.insertionIndex;
  
  console.log('Card moved from', movedFrom, 'to', movedTo, 'at index', newIndex);
});

10. Use the FileDrop Plugin:

const container = document.getElementById('upload-area');

// Initialize Snap with FileDrop plugin
const snap = new Snap(container).use(
  new FileDrop({
    // MIME types or extensions to accept
    accept: ['image/*', '.pdf'],
    // Allow multiple files
    multiple: true,
    // Maximum file size in bytes (10MB)
    maxSize: 10 * 1024 * 1024
  }).onFileDrop((e) => {
    console.log('Files dropped:', e.files);
    console.log('Drop zone:', e.dropZone);
    
    // Process files
    for (const file of e.files) {
      console.log('File name:', file.name);
      console.log('File size:', file.size);
      console.log('File type:', file.type);
    }
  })
);
// OR using the standalone file drop helper:
import { createFileDropZone } from 'snap-dnd';

const element = document.getElementById('drop-zone');

// Create a file drop zone without full Snap instance
const cleanup = createFileDropZone(element, {
  // Accept only images
  accept: ['image/*'],
  // Single file only
  multiple: false,
  // Process dropped files onDrop: (files) => {
    console.log('Received files:', files);
    uploadFiles(files);
  },
  // Optional callbacks
  onDragEnter: () => {
    element.classList.add('drag-active');
  },
  onDragLeave: () => {
    element.classList.remove('drag-active');
  }
});

// Clean up when done
cleanup();

11. Add custom behaviors that extend core functionality with reusable features:

import { Snap, AutoScroll, SnapGrid } from 'snap-dnd';

const snap = new Snap(container)
// Auto-scroll when dragging near viewport edges
.addBehavior(new AutoScroll({
  threshold: 50,  // pixels from edge to trigger scroll
  maxSpeed: 20,   // maximum scroll speed
  acceleration: 2 // acceleration curve exponent
}))
// Snap coordinates to grid
.addBehavior(new SnapGrid({
  x: 10,  // horizontal grid size
  y: 10   // vertical grid size
}));

12. Create complex layouts where sections move vertically and items within sections move horizontally:

<div id="board">
  <!-- Each section is draggable vertically and acts as a drop zone -->
  <div class="section" data-draggable data-drag-axis="y" data-droppable>
    <!-- Items within sections are draggable horizontally -->
    <div class="item" data-draggable data-drag-axis="x">1</div>
    <div class="item" data-draggable data-drag-axis="x">2</div>
    <div class="item" data-draggable data-drag-axis="x">3</div>
  </div>
  <div class="section" data-draggable data-drag-axis="y" data-droppable>
    <div class="item" data-draggable data-drag-axis="x">4</div>
    <div class="item" data-draggable data-drag-axis="x">5</div>
    <div class="item" data-draggable data-drag-axis="x">6</div>
  </div>
</div>
const snap = new Snap(document.getElementById('board'), {
  onDrop: (e) => {
    // Determine if a section or item was moved
    const isSection = e.element.classList.contains('section');
    const entityType = isSection ? 'Section' : 'Item';
    console.log(entityType, 'moved to index:', e.insertionIndex);
  }
}).use(new Sortable());

Alternatives:

FAQs:

Q: How do I prevent specific elements from being dragged?
A: Remove the data-draggable attribute or use snap.removeDraggable(element) to disable dragging programmatically. You can also add conditional logic in the onDragStart callback and call e.cancel() to prevent specific drag operations.

Q: Can I use Snap with server-side rendered content that hydrates client-side?
A: Yes. Initialize Snap after hydration completes. The library scans for declarative attributes on initialization. If you add elements dynamically after initialization, call snap.refresh() or enable autoRefresh: true in the options.

Q: How do I persist the new order after dropping an item?
A: The onDrop callback provides an insertionIndex property. Use this index to reorder your data array, then update the DOM or trigger a framework re-render.

Q: Why is the drag ghost positioned incorrectly inside scrollable containers?
A: The drag ghost uses fixed positioning relative to the viewport. If your container has transform, perspective, or filter CSS properties, these create new positioning contexts. Remove these properties from ancestor elements or use the renderGhost option to customize ghost positioning.

Q: Can I drag elements between different Snap instances?
A: No. Each Snap instance manages an isolated drag-drop context. To move elements between containers, use a single Snap instance that wraps both containers and implement the Kanban plugin for cross-container drops.

The post Tiny & Memory-optimized Drag and Drop Library – snap-dnd appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading