Basic Drag and Drop

Implementing Basic Drag and Drop

Welcome to the first lesson of our Advanced Interactions and SvelteKit Features course. Throughout this course, we'll transform your existing Kanban board application into a professional-grade tool with fluid drag and drop interactions, advanced routing capabilities and server-side rendering optimizations.

Drag and drop functionality is one of the most intuitive ways users can interact with digital interfaces. In the context of a Kanban board, being able to physically move task cards between columns creates a natural workflow that mirrors how people organize physical sticky notes on a wall. This interaction pattern reduces cognitive load and makes task management feel effortless and engaging.

The HTML5 Drag and Drop API provides native browser support for these interactions, offering events like dragstart, dragover, and drop that we can hook into. While third-party libraries like svelte-dnd-action can simplify implementation, understanding the underlying API gives you complete control over the user experience and helps you create custom interactions tailored to your specific needs.

In this lesson, we'll implement basic drag and drop functionality that allows users to drag task cards from one column to another, automatically updating the task's status. When a user drags a "Learn Svelte 5" task from the "To Do" column to "In Progress," the task's status will update immediately, and the change will be persisted to localStorage. You'll see visual feedback during the drag operation, including hover states on drop zones and smooth transitions.

This foundation will prepare you for the advanced features we'll build in subsequent lessons, including drag handles for better accessibility, reordering tasks within the same column, and enhanced animations that make the interface feel polished and responsive.

What You'll Build: Visual Drag and Drop Experience

In this lesson, we'll enhance your existing Kanban board with smooth drag and drop functionality. Let's see exactly how the user experience will transform.

Your Current Board (Static Experience): Current Kanban Board

Right now, your board looks great and functions well, but users must click action buttons to move tasks between columns. While this works, it doesn't feel as natural as directly moving the visual task cards.

After This Lesson (Interactive Drag Experience): Kanban Board with Drag and Drop

Notice the visual changes during drag operations:

  • The dragged task card becomes semi-transparent and slightly smaller
  • The target column highlights with a blue border and subtle background tint
  • A dashed placeholder shows exactly where the task will be dropped
  • The cursor changes to indicate an active drag operation

Key Visual Enhancements You'll Implement:

  • Drag Feedback: Cards become translucent and scale down when being dragged
  • Drop Zone Highlighting: Columns glow with your theme's primary color when valid drop targets
  • Precise Positioning: Placeholder indicators show exactly where tasks will land
  • Smooth Transitions: All state changes animate smoothly for professional polish
  • Cursor Changes: Visual cues indicate when elements are draggable and being dragged

These visual improvements transform the static board into an interactive workspace where moving tasks feels as natural as organizing physical sticky notes.

Setting Up the Drag State Store

Before implementing the actual drag and drop interactions, we need to create a centralized state management system to track drag operations. This approach keeps our drag logic organized and makes it easy to coordinate between different components during drag operations.

Let's create a new file called src/lib/dndStore.svelte.js to manage our drag and drop state. This store will use Svelte's $state rune to create reactive state that automatically updates our UI when drag operations begin, progress, and complete.

Important Note: Notice we're using the .svelte.js file extension here. This double extension is part of Svelte's file naming convention — when you're creating pure JavaScript stores or utilities that contain Svelte reactivity (like $state runes), you should use the .svelte.js extension. This tells SvelteKit to process the file for Svelte's reactive features while keeping it as a JavaScript module rather than a component.

import { showSuccess } from './notificationStore.svelte.js';
import { updateTaskStatus } from './taskStore.svelte.js';

// Track drag state
export const dragState = $state({
  isDragging: false,
  draggedTask: null,
  draggedElement: null,
  sourceColumn: null,
  placeholder: null
});

The dragState object contains all the information we need to track during drag operations. The isDragging boolean tells us whether a drag operation is currently active, which we'll use to show or hide visual feedback. The draggedTask stores the actual task object being moved, while draggedElement keeps a reference to the DOM element for styling purposes. The sourceColumn and targetStatus track where the task started and where it's being moved to, and placeholder will hold a reference to a visual placeholder element.

Implementing Drag Initialization

Now let's add the core functions that will manage the drag lifecycle. The first function we need handles the start of drag operations when users begin dragging a task card.

Add this function to your src/lib/dndStore.svelte.js file:

// Start dragging
export function startDrag(event, task, status) {
  dragState.isDragging = true;
  dragState.draggedTask = task;
  dragState.draggedElement = event.target;
  dragState.sourceColumn = status;
  
  // Set drag data
  event.dataTransfer.effectAllowed = 'move';
  event.dataTransfer.setData('text/plain', JSON.stringify({
    taskId: task.id,
    sourceStatus: status
  }));
  
  // Add dragging class after a small delay to prevent immediate visual change
  setTimeout(() => {
    if (dragState.draggedElement) {
      dragState.draggedElement.classList.add('dragging');
    }
  }, 0);
}

The startDrag function initializes our drag operation. It updates our reactive state and configures the browser's drag and drop system through the dataTransfer object. The effectAllowed property tells the browser this is a move operation, and setData stores the task information that will be available when the item is dropped. The small timeout before adding the CSS class prevents visual flickering that can occur when the drag starts.

Adding Drag Events to Task Cards

With our drag initialization function ready, let's modify the TaskCard component to support dragging. We'll need to add the draggable attribute and implement the necessary event handlers to initiate and manage drag operations.

First, let's update the script section of src/components/TaskCard.svelte to import our drag functions and add the draggable functionality:

<script>
  import { startDrag, endDrag } from '$lib/dndStore.svelte.js';
  
  let { id, title, description = '', status draggable = true } = $props();
  
  function handleDragStart(event) {
    const task = { id, title, description };
    startDrag(event, task, status);
  }
  
  function handleDragEnd(event) {
    endDrag();
  }
</script>

The script section imports our drag functions and creates event handlers that will be called when drag operations begin and end. The handleDragStart function creates a task object with the necessary information and passes it to our startDrag function along with the drag event and current status.

Making Task Cards Draggable in the Template

Now let's update the template to make the card draggable:

<div 
  class="task-card" 
  draggable={draggable && !loadingStates.moveTask}
  ondragstart={handleDragStart}
  ondragend={handleDragEnd}
  role="article"
  aria-label="Task: {title}"
>
  <h3>{title}</h3>
  {#if description}
    <p>{description}</p>
  {/if}
</div>

The draggable attribute tells the browser that this element can be dragged. In Svelte, we use ondragstart and ondragend to attach our event handlers. The ondragstart event fires when the user begins dragging the element, while ondragend fires when the drag operation completes, regardless of whether it was successful.

Styling Draggable Elements

We need to add CSS styles to provide visual feedback during drag operations. Add these styles to your TaskCard.svelte component:

.task-card {
  background: var(--color-cardBg);
  padding: 1rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 0.5rem;
  user-select: none;
  cursor: move;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.task-card:hover {
  transform: translateY(-2px);
}

.task-card.dragging {
  opacity: 0.5;
  transform: scale(0.95);
  cursor: grabbing;
}

The CSS includes a hover effect that lifts the card slightly when the user hovers over it, indicating that it's interactive. The .dragging class reduces opacity and scales down the card during drag operations, providing clear visual feedback that the item is being moved. The user-select: none property prevents text selection when dragging, and cursor: move indicates that the element can be moved.

When a user starts dragging a task card, they'll see the card become semi-transparent and slightly smaller, while the cursor changes to indicate a drag operation is in progress. This immediate visual feedback helps users understand that their action has been recognized and the drag operation has begun.

Creating Drop Zone Event Handlers

Now we need to convert our columns into valid drop zones that can receive dragged tasks. This involves implementing several drag event handlers and providing visual feedback when users hover over potential drop targets.

Let's add the core drop zone functions to our src/lib/dndStore.svelte.js file:

// Handle drag over
export function handleDragOver(event, targetStatus) {
  event.preventDefault();
  event.dataTransfer.dropEffect = 'move';
  
  const dropzone = event.currentTarget;
  dropzone.classList.add('drag-over');
  
  // Update placeholder position
  const afterElement = getDragAfterElement(dropzone, event.clientY);
  const placeholder = dragState.placeholder;
  
  if (placeholder) {
    if (afterElement == null) {
      dropzone.appendChild(placeholder);
    } else {
      dropzone.insertBefore(placeholder, afterElement);
    }
  }
}

// Handle drag leave
export function handleDragLeave(event) {
  if (event.currentTarget === event.target) {
    event.currentTarget.classList.remove('drag-over');
  }
}

The handleDragOver function is crucial because it prevents the browser's default behavior and tells it that this element can accept drops. The dropEffect property provides visual feedback to the user about what will happen when they drop the item. We also add a CSS class to style the drop zone and manage placeholder positioning.

Implementing Drop Zone Templates

With our event handlers ready, let's update the Column component to use them. We'll add the necessary attributes and styling to make columns function as proper drop zones.

Update the script section of src/components/Column.svelte to import the drag functions and create wrapper handlers:

<script>
  import TaskCard from './TaskCard.svelte';
  import EmptyState from './EmptyState.svelte';
  import LoadingIndicator from './LoadingIndicator.svelte';
  import { loadingStates } from '$lib/loadingStore.svelte.js';
  import { 
    handleDragOver, 
    handleDragLeave, 
    handleDrop,
    createPlaceholder
  } from '$lib/dndStore.svelte.js';
  
  let { title, tasks = [], status, taskCount = 0, updateTasks } = $props();
  
  // Handle drag events
  function onDragOver(event) {
    if (!loadingStates.tasks) {
      handleDragOver(event, status);
    }
  }
  
  function onDragLeave(event) {
    handleDragLeave(event);
  }
  
  function onDrop(event) {
    handleDrop(event, status, updateTasks);
  }
  
  function onDragEnter(event) {
    event.preventDefault();
    // Create placeholder on first enter
    if (!document.querySelector('.drag-placeholder')) {
      createPlaceholder();
    }
  }
</script>

Now let's update the template to include the drop zone functionality:

<div class="column">
  <h2>
    {title}
    <span class="count">{taskCount}</span>
  </h2>
  
  <div 
    class="tasks dropzone"
    ondragover={onDragOver}
    ondragleave={onDragLeave}
    ondrop={onDrop}
    ondragenter={onDragEnter}
    role="region" 
    aria-label="{title} column with {taskCount} tasks"
  >
    {#if loadingStates.tasks}
      <div class="loading-container">
        <LoadingIndicator type="tasks" />
        <span>Loading tasks...</span>
      </div>
    {:else if tasks.length === 0}
      <EmptyState type={status} />
    {:else}
      {#each tasks as task (task.id)}
        <TaskCard 
          id={task.id}
          title={task.title} 
          description={task.description}
          status={status}
          draggable={!loadingStates.tasks}
        />
      {/each}
    {/if}
  </div>
</div>

The key addition is the dropzone class and the drag event handlers on the tasks container. We also disable dragging when tasks are loading to prevent conflicts during data operations.

Adding Drop Zone Visual Feedback

Visual feedback during drag operations is essential for creating an intuitive user experience. Let's add the CSS styles that will provide visual feedback when users drag items over columns.

Add these styles to your Column.svelte component:

.column {
  background: var(--color-columnBg);
  padding: 1rem;
  width: 300px;
  min-height: 400px;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  transition: transform 0.2s ease;
}

.column:hover {
  transform: translateY(-5px);
}

Let's also add the global styles for drag and drop feedback. Add these to your src/routes/+layout.svelte file:

/* Custom DnD styling */
:global(.task-card) {
  cursor: move;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

:global(.task-card:hover) {
  transform: translateY(-2px);
}

:global(.task-card.dragging) {
  opacity: 0.5;
  transform: scale(0.95);
  cursor: grabbing;
}

:global(.dropzone) {
  transition: background-color 0.2s ease, outline 0.2s ease;
}

:global(.dropzone.drag-over) {
  outline: 2px solid var(--color-primary);
  background-color: color-mix(in srgb, var(--color-primary) 5%, var(--color-columnBg));
}

:global(.drag-placeholder) {
  transition: all 0.2s ease;
}

These styles create clear visual indicators when elements are draggable and when drag operations are active. The transitions ensure smooth animations that make the interface feel polished and professional.

Processing Drop Events

The drop operation is where the magic happens — this is where we actually process the dragged task and update its status in our application. We need to extract the task information from the drop event, validate the operation, and integrate it with our existing task management system.

Let's complete the handleDrop function in our drag and drop store. Add this function to src/lib/dndStore.svelte.js:

// Handle drop
export async function handleDrop(event, targetStatus, updateTasksCallback) {
  event.preventDefault();
  
  const dropzone = event.currentTarget;
  dropzone.classList.remove('drag-over');
  
  try {
    const data = JSON.parse(event.dataTransfer.getData('text/plain'));
    const { taskId, sourceStatus } = data;
    
    // Only process if moved to different column
    if (sourceStatus !== targetStatus) {
      await updateTaskStatus(taskId, targetStatus);
      showSuccess(`Task moved to ${formatStatus(targetStatus)}`);
      
      // Update the UI through callback
      if (updateTasksCallback) {
        updateTasksCallback();
      }
    }
  } catch (error) {
    console.error('Error handling drop:', error);
  }
  
  // Clean up
  endDrag();
}

// Helper function to format status for display
function formatStatus(status) {
  switch (status) {
    case 'todo': return 'To Do';
    case 'inprogress': return 'In Progress';
    case 'done': return 'Done';
    default: return status;
  }
}

The function starts by preventing the browser's default drop behavior and removing any visual styling from the drop zone. It then attempts to parse the task data that was stored during the drag start operation. If the task is being moved to a different column, it calls our existing updateTaskStatus function to persist the change and shows a success notification.

The error handling ensures that any issues during the drop operation are logged but don't crash the application. The cleanup happens regardless of whether the operation succeeded, ensuring our UI stays in a consistent state.

Implementing Drag Cleanup

Proper cleanup is essential for maintaining a stable drag and drop system. We need to reset our state and remove any temporary visual elements when drag operations complete, whether they succeed or are cancelled.

Add the cleanup function to your src/lib/dndStore.svelte.js file:

// End dragging
export function endDrag() {
  if (dragState.draggedElement) {
    dragState.draggedElement.classList.remove('dragging');
  }
  
  // Remove placeholder
  if (dragState.placeholder && dragState.placeholder.parentNode) {
    dragState.placeholder.parentNode.removeChild(dragState.placeholder);
  }
  
  // Reset state
  dragState.isDragging = false;
  dragState.draggedTask = null;
  dragState.draggedElement = null;
  dragState.sourceColumn = null;
  dragState.placeholder = null;
}

The endDrag function performs comprehensive cleanup by removing all temporary CSS classes, cleaning up any placeholder elements, and resetting our reactive state. The function is called both when drops succeed and when they're cancelled (such as when a user drags an item outside any valid drop zone), ensuring our application returns to a clean state regardless of how the drag operation ends.

This centralized cleanup approach ensures that all components involved in drag and drop operations stay synchronized and that cleanup happens consistently.

Updating the Board Component

To ensure our Board component can respond to drag and drop operations, we need to provide an update callback that will refresh the UI after successful drops.

Update src/components/Board.svelte to provide the necessary callback:

<script>
  import Column from './Column.svelte';
  import EmptyState from './EmptyState.svelte';
  import LoadingIndicator from './LoadingIndicator.svelte';
  import { 
    getTodoTasks, 
    getInProgressTasks, 
    getDoneTasks,
    loadTasks
  } from '$lib/taskStore.svelte.js';
  import { loadingStates } from '$lib/loadingStore.svelte.js';
  
  // Force update function to pass to columns
  function forceUpdate() {
    // This will trigger reactivity by accessing the derived values
    getTodoTasks();
    getInProgressTasks();
    getDoneTasks();
  }
</script>

<div class="board">
  <Column 
    title="To Do" 
    tasks={getTodoTasks()} 
    status="todo"
    taskCount={getTodoTasks().length}
    updateTasks={forceUpdate}
  />
  <Column 
    title="In Progress" 
    tasks={getInProgressTasks()} 
    status="inprogress"
    taskCount={getInProgressTasks().length}
    updateTasks={forceUpdate}
  />
  <Column 
    title="Done" 
    tasks={getDoneTasks()} 
    status="done"
    taskCount={getDoneTasks().length}
    updateTasks={forceUpdate}
  />
</div>

<style>
  .board {
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    padding: 1rem;
  }
  
  @media (max-width: 950px) {
    .board {
      flex-direction: column;
    }
  }
</style>

The forceUpdate function ensures that our UI refreshes after drag and drop operations complete. While Svelte's reactivity system typically handles updates automatically, drag and drop operations can sometimes require explicit updates to ensure the interface reflects the new state immediately.

When a user successfully drops a task in a new column, they'll see the task disappear from its original location and appear in the new column. They'll also receive a notification confirming the action, such as "Task moved to In Progress." If there's an error during the operation, it will be logged to the console, and the drag state will still be cleaned up properly.

Creating placeholder for visual feedback

Now let's implement the placeholder functionality that shows users exactly where their task will be dropped. Add this function to src/lib/dndStore.svelte.js:

// Create placeholder element
export function createPlaceholder() {
  const placeholder = document.createElement('div');
  placeholder.className = 'drag-placeholder';
  placeholder.style.height = '80px';
  placeholder.style.marginBottom = '0.5rem';
  placeholder.style.border = '2px dashed var(--color-primary)';
  placeholder.style.borderRadius = '8px';
  placeholder.style.opacity = '0.5';
  dragState.placeholder = placeholder;
  return placeholder;
}

// Get element after which to insert
function getDragAfterElement(container, y) {
  const draggableElements = [...container.querySelectorAll('.task-card:not(.dragging)')];
  
  return draggableElements.reduce((closest, child) => {
    const box = child.getBoundingClientRect();
    const offset = y - box.top - box.height / 2;
    
    if (offset < 0 && offset > closest.offset) {
      return { offset: offset, element: child };
    } else {
      return closest;
    }
  }, { offset: Number.NEGATIVE_INFINITY }).element;
}

The createPlaceholder function generates a visual indicator that shows where the dragged task will be inserted. The getDragAfterElement function calculates the optimal position for this placeholder based on the mouse cursor's vertical position, creating a smooth insertion experience.

Summary and Preparation for Advanced Features

Congratulations! You've successfully implemented a complete basic drag and drop system for your Kanban board. Your task cards are now fully draggable between columns, with automatic status updates, visual feedback, and proper cleanup. Users can intuitively move tasks by dragging them from one column to another, and the changes are immediately reflected in the interface and persisted to localStorage.

Let's review what we've accomplished in this lesson. We created a centralized drag and drop state management system using Svelte's $state rune that tracks all aspects of drag operations. We made task cards draggable by adding the necessary HTML attributes and event handlers, providing immediate visual feedback when drag operations begin. We converted our columns into proper drop zones that highlight when dragged items hover over them and show placeholder indicators for precise positioning. Finally, we implemented robust drop handling that integrates with our existing task management system and provides user feedback through notifications.

The foundation we've built is solid and extensible. Our drag and drop system properly handles edge cases, provides comprehensive visual feedback, and integrates seamlessly with the existing application architecture. The code is organized and maintainable, making it easy to add new features and enhancements.

In Unit 2, we'll enhance these interactions significantly. You'll learn how to implement drag handles that give users more control over when dragging is initiated, add the ability to reorder tasks within the same column, and create more sophisticated animations and transitions. We'll also explore how to make the drag and drop experience more accessible and touch-friendly.

Unit 3 will introduce SvelteKit routing, allowing you to create multiple views of your tasks and implement deep linking to individual task details.

The practice exercises that follow this lesson will give you hands-on experience implementing these drag and drop features. You'll start with basic dragging functionality and progressively add more sophisticated features, reinforcing the concepts covered in this lesson while building confidence in your implementation skills. Take your time with the exercises, and don't hesitate to experiment with different approaches to deepen your understanding of the drag and drop system we've created.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal