Enhancing Drag Interactions

Introduction: Elevating Basic Drag and Drop to Professional Standards

In Unit 1, you successfully implemented a functional drag and drop system that allows users to move task cards between columns in your Kanban board. While this foundation works well, there's a significant difference between functional drag and drop and the polished, professional interactions that users expect from modern web applications. In this lesson, we'll transform your basic implementation into a sophisticated system that feels smooth, intuitive, and delightful to use.

The drag and drop system you built in the previous lesson handles the core functionality perfectly — users can drag tasks from one column to another, and the status updates automatically. However, you may have noticed some limitations. When users want to expand a task card to see its details, they might accidentally trigger a drag operation. There's no way to reorder tasks within the same column, and the visual feedback, while functional, could be more polished and informative.

Professional drag interactions address these concerns through several key enhancements. Drag handles provide users with precise control over when dragging is initiated, separating the drag action from other card interactions like clicking to expand. Same-column reordering allows users to prioritize tasks within their current status, creating a more complete task management experience. Enhanced visual feedback, including smooth animations and better placeholder positioning, makes the interface feel responsive and engaging.

The enhancements we'll implement in this lesson build directly on the foundation you created in Unit 1. We'll extend your existing dndStore.svelte.js to support new interaction modes, enhance your TaskCard component with optional drag handles, and add sophisticated animations using Svelte's built-in animation features. By the end of this lesson, your Kanban board will have the kind of polished drag interactions that users associate with professional productivity applications.

These improvements aren't just about aesthetics — they significantly improve usability and user satisfaction. When drag interactions feel smooth and predictable, users can focus on their tasks rather than fighting with the interface. The ability to customize drag behavior through user preferences ensures that your application can accommodate different user preferences and accessibility needs.

Adding Drag Handle State Management

The first step in implementing drag handles is extending our drag state management to support this new interaction mode. We need to add a preference setting that controls whether users see drag handles or can drag from anywhere on the task card.

Let's update our drag store to support drag handle preferences. Open your src/lib/dndStore.svelte.js file and modify the dragState object:

// Track drag state
let dragState = $state({
  isDragging: false,
  draggedTask: null,
  draggedElement: null,
  sourceColumn: null,
  placeholder: null,
  draggedOverTask: null,
  useDragHandle: true // Option for drag handles
});

The useDragHandle property will control whether our task cards display drag handles or allow dragging from anywhere on the card. Setting it to true by default provides the more controlled experience, but we'll give users the option to change this preference.

Now we need to add a function to toggle this preference. Add this function to your drag store:

// Toggle drag handle setting
export function toggleDragHandleMode(useHandle) {
  dragState.useDragHandle = useHandle;
}

This simple function allows other components to change the drag handle preference. The reactive nature of Svelte's $state rune means that any components using this value will automatically update when it changes, providing immediate feedback when users toggle their preferences.

Updating Task Card Script for Drag Handles

Now we need to modify the TaskCard component to support the new drag handle functionality. This involves updating the event handlers to respect the drag handle preference and prevent conflicts between different interaction modes.

Open src/components/TaskCard.svelte and update the script section:

<script>
  import { slide } from 'svelte/transition';
  import TaskActions from './TaskActions.svelte';
  import DeleteButton from './DeleteButton.svelte';
  import { startDrag, endDrag, handleDragOverTask } from '$lib/dndStore.svelte.js';
  
  let { id, title, description = '', status, useHandle = true } = $props();
  let isExpanded = $state(false);
  
  function toggleExpand(e) {
    // Don't expand when clicking on drag handle
    if (useHandle && e.target.closest('.drag-handle')) {
      return;
    }
    isExpanded = !isExpanded;
  }
  
  function handleDragStart(event) {
    // Check if dragging from handle when handles are enabled
    if (useHandle && !event.target.closest('.drag-handle')) {
      event.preventDefault();
      return;
    }
    
    const task = { id, title, description };
    startDrag(event, task, status);
  }
  
  function handleDragEnd(event) {
    endDrag();
  }
  
  function handleDragOver(event) {
    const task = { id, title, description };
    handleDragOverTask(event, task, status);
  }
</script>

The key changes here involve the toggleExpand function and the handleDragStart function. The toggleExpand function now checks if the click originated from a drag handle and prevents expansion in that case. The handleDragStart function validates that dragging is initiated from the appropriate area when handles are enabled.

The useHandle prop allows parent components to control whether this specific task card should use drag handles, providing flexibility in how the preference is applied throughout your application.

Adding Drag Handle Templates and Markup

With the script logic updated, we need to modify the task card template to conditionally render drag handles and adjust the draggable behavior based on the current preference setting.

Update the template section of your TaskCard.svelte component:

<div 
  class="task-card" 
  data-task-id={id}
  draggable={!useHandle}
  ondragstart={handleDragStart}
  ondragend={handleDragEnd}
  ondragover={handleDragOver}
  tabindex="0" 
  onclick={toggleExpand}
  in:slide|local={{ duration: 300 }}
  role="article"
  aria-roledescription="Task"
  aria-expanded={isExpanded}
>
  <div class="task-header">
    <h3 id="task-title-{id}">{title}</h3>
    
    {#if useHandle}
      <div 
        class="drag-handle" 
        draggable="true"
        ondragstart={handleDragStart}
        role="button"
        tabindex="0"
        aria-label="Drag to reorder task: {title}"
        aria-describedby="task-title-{id}"
      >
        <svg 
          viewBox="0 0 24 24" 
          width="16" 
          height="16"
          aria-hidden="true"
          focusable="false"
        >
          <path fill="currentColor" d="M8 18h8v-2H8v2zm0-4h8v-2H8v2zm0-4h8V8H8v2zm-4 8h2V8H4v10z" />
        </svg>
      </div>
    {/if}
  </div>
  
  {#if description && isExpanded}
    <p class="description" in:slide|local>
      {description}
    </p>
  {/if}
  
  {#if isExpanded}
    <div class="task-controls" in:slide|local>
      <TaskActions 
        {id}
        {status}
      />
      <DeleteButton {id} />
    </div>
  {/if}
</div>

Notice how the main card element is only draggable when useHandle is false. When handles are enabled, only the drag handle itself is draggable. The SVG icon provides a visual cue that this area can be used for dragging. The accessibility attributes ensure that screen readers can understand the purpose of the drag handle.

The conditional rendering with {#if useHandle} means the drag handle only appears when that mode is active, keeping the interface clean and uncluttered when users prefer full-card dragging.

Styling Drag Handles

Visual design is crucial for drag handles - they need to be clearly identifiable as interactive elements while not overwhelming the rest of the task card design. Let's add the CSS styles that will make the drag handles both functional and visually appealing.

Add these styles to your TaskCard.svelte component:

.task-card {
  padding: 0.75rem;
  border-radius: 6px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.3s ease;
  cursor: pointer;
  background-color: var(--color-cardBg);
}

.task-card:hover, .task-card:focus {
  transform: translateY(-2px);
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}

.task-card:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.task-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

h3 {
  margin-top: 0;
  margin-bottom: 0.5rem;
  font-size: 1rem;
  flex: 1;
}

.drag-handle {
  color: var(--color-secondary);
  cursor: grab;
  padding: 0.25rem;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.drag-handle:hover {
  background-color: var(--color-muted);
  color: var(--color-primary);
}

.drag-handle:active {
  cursor: grabbing;
}

.description {
  margin: 0 0 0.75rem 0;
  font-size: 0.875rem;
  color: var(--color-secondary);
}

.task-controls {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-top: 0.5rem;
}

The drag handle styles make it visually distinct from the rest of the card while maintaining consistency with your design system. The cursor changes from grab to grabbing to provide immediate feedback about the interaction state. The hover effect makes it clear that this element is interactive.

Here's a how it will look once implemented:

Adding Reorder Functionality to Task Store

Before we can implement same-column reordering in the drag interface, we need to add the underlying functionality to our task store. This function will handle the complex logic of reordering tasks within a single column.

Open src/lib/taskStore.svelte.js and add this function:

// Add method for reordering tasks in a column
export async function reorderTasks(status, newOrder) {
  return withLoading('moveTask', async () => {
    try {
      // Filter tasks by status
      const columnTasks = tasks.filter(task => task.status === status);
      
      // Create a map of task id -> task for quick lookup
      const taskMap = new Map();
      columnTasks.forEach(task => taskMap.set(task.id, task));
      
      // Create a new ordered array with tasks in new position
      const reorderedTasks = newOrder.map(id => taskMap.get(parseInt(id))).filter(Boolean);
      
      // Get the ids of tasks in their original order
      const originalIds = columnTasks.map(task => task.id);
      
      // Update task positions
      if (JSON.stringify(originalIds) !== JSON.stringify(newOrder)) {
        // Remove tasks of this status
        for (let i = tasks.length - 1; i >= 0; i--) {
          if (tasks[i].status === status) {
            tasks.splice(i, 1);
          }
        }
        
        // Add tasks back in new order
        reorderedTasks.forEach(task => tasks.push(task));
        
        await saveTasks();
        showSuccess('Task order updated');
      }
      
      return true;
    } catch (error) {
      showError(`Failed to reorder tasks: ${error.message}`);
      throw error;
    }
  });
}

This function handles the complex logic of reordering tasks within a column. It creates a map for efficient task lookup, compares the new order with the original order to detect changes, and then reconstructs the tasks array with the new ordering. The function integrates with your existing loading states and notification system to provide consistent user feedback.

Implementing Task-Level Drag Detection

To enable precise reordering within columns, we need to detect when users are dragging over specific tasks (not just over columns in general). This allows us to show placeholder indicators between tasks and calculate exact insertion positions.

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

// Handle drag over on task
export function handleDragOverTask(event, overTask, targetStatus) {
  event.preventDefault();
  event.stopPropagation();
  
  if (!dragState.draggedTask || dragState.draggedTask.id === overTask.id) {
    return;
  }
  
  const taskElement = event.currentTarget;
  const rect = taskElement.getBoundingClientRect();
  const midpoint = rect.top + rect.height / 2;
  const insertBefore = event.clientY < midpoint;
  
  // Remove existing placeholder
  const existingPlaceholder = document.querySelector('.drag-placeholder');
  if (existingPlaceholder) {
    existingPlaceholder.remove();
  }
  
  // Create and insert new placeholder
  const placeholder = createPlaceholder();
  if (insertBefore) {
    taskElement.parentNode.insertBefore(placeholder, taskElement);
  } else {
    taskElement.parentNode.insertBefore(placeholder, taskElement.nextSibling);
  }
  
  dragState.draggedOverTask = { task: overTask, insertBefore };
}

This function handles the precise positioning logic for same-column reordering. It calculates whether the dragged task should be inserted before or after the task being hovered over based on the mouse position relative to the task's midpoint. The placeholder provides immediate visual feedback about where the task will be positioned.

Updating Drop Logic for Reordering

Now we need to update our drop handling logic to support both cross-column moves and same-column reordering. The enhanced drop handler needs to distinguish between these two scenarios and handle them appropriately.

Modify the handleDrop function in your src/lib/dndStore.svelte.js file:

// Handle drop
export async function handleDrop(event, targetStatus) {
  event.preventDefault();
  event.stopPropagation();
  
  const dropzone = event.currentTarget;
  dropzone.classList.remove('drag-over');
  
  try {
    const data = JSON.parse(event.dataTransfer.getData('text/plain'));
    const { taskId, sourceStatus } = data;
    
    // Get all tasks in the target column
    const taskElements = Array.from(dropzone.querySelectorAll('.task-card:not(.dragging)'));
    const placeholder = dropzone.querySelector('.drag-placeholder');
    
    if (sourceStatus === targetStatus && placeholder) {
      // Same column reordering
      const placeholderIndex = Array.from(dropzone.children).indexOf(placeholder);
      const tasksInColumn = taskElements.map(el => {
        const id = parseInt(el.getAttribute('data-task-id'));
        return id;
      });
      
      // Remove the dragged task from the array
      const draggedIndex = tasksInColumn.indexOf(taskId);
      if (draggedIndex > -1) {
        tasksInColumn.splice(draggedIndex, 1);
      }
      
      // Insert at new position
      const insertIndex = Math.max(0, placeholderIndex);
      tasksInColumn.splice(insertIndex, 0, taskId);
      
      // Reorder tasks
      await reorderTasks(targetStatus, tasksInColumn);
      showSuccess('Task order updated');
    } else if (sourceStatus !== targetStatus) {
      // Different column - update status
      await updateTaskStatus(taskId, targetStatus);
      showSuccess(`Task moved to ${formatStatus(targetStatus)}`);
    }
  } catch (error) {
    console.error('Error handling drop:', error);
  }
  
  // Clean up
  endDrag();
}

This enhanced drop handler distinguishes between same-column reordering and cross-column moves. For same-column operations, it calculates the new task order based on the placeholder position and calls the reorderTasks function. For cross-column moves, it uses the existing status update logic.

Adding Flip Animations to Columns

Smooth animations make reordering operations feel natural and help users understand what's happening when tasks change position. Svelte's built-in flip animation automatically creates smooth transitions when list items change order.

Let's update the Column component to include flip animations and enhanced visual feedback. Open src/components/Column.svelte and update the script section:

<script>
  import { flip } from 'svelte/animate';
  import TaskCard from './TaskCard.svelte';
  import EmptyState from './EmptyState.svelte';
  import LoadingIndicator from './LoadingIndicator.svelte';
  import { loadingStates } from '$lib/loadingStore.svelte.js';
  import { 
    handleDragOverColumn, 
    handleDragLeave, 
    handleDrop,
    dragState
  } from '$lib/dndStore.svelte.js';
  
  let { title, tasks = [], status, taskCount = 0 } = $props();
  
  // Get loading state for this specific status
  const isLoading = $derived(loadingStates.tasks || 
    (status === 'todo' ? loadingStates.addTask : false));
  
  // Check if list is empty
  const isEmpty = $derived(tasks.length === 0);
  
  // Handle drag events
  function onDragOver(event) {
    if (!isLoading) {
      handleDragOverColumn(event, status);
    }
  }
  
  function onDragLeave(event) {
    handleDragLeave(event);
  }
  
  function onDrop(event) {
    handleDrop(event, status);
  }
  
  // Highlight when dragging
  const isHighlighted = $derived(
    dragState.isDragging && dragState.sourceColumn !== status
  );
  
  // Get flip transition duration
  const flipDurationMs = 300;
</script>

The key additions here are the import of the flip animation and the isHighlighted derived value that determines when to show enhanced visual feedback. The flipDurationMs constant controls the duration of reordering animations.

Implementing Enhanced Column Templates

Now let's update the column template to include the flip animations and enhanced visual feedback. The template needs to wrap each task in an element that can be animated and apply conditional styling for drag highlighting.

Update the template section of your Column.svelte component:

<div class="column" class:column-highlight={isHighlighted}>
  <h2>
    {title}
    <span class="count">{taskCount}</span>
  </h2>
  
  <div 
    class="tasks dropzone"
    ondragover={onDragOver}
    ondragleave={onDragLeave}
    ondrop={onDrop}
    role="region" 
    aria-label="{title} column with {taskCount} tasks"
  >
    {#if isLoading}
      <div class="loading-container">
        <LoadingIndicator type="tasks" />
        <span>Loading tasks...</span>
      </div>
    {:else if isEmpty}
      <EmptyState type={status} />
    {:else}
      {#each tasks as task (task.id)}
        <div 
          class="dnd-item" 
          animate:flip={{ duration: flipDurationMs }}
        >
          <TaskCard 
            id={task.id}
            title={task.title} 
            description={task.description}
            status={status}
            useHandle={dragState.useDragHandle}
          />
        </div>
      {/each}
    {/if}
  </div>
</div>

The animate:flip directive automatically creates smooth animations when tasks change position due to reordering. The class:column-highlight directive conditionally applies enhanced styling when the column is a valid drop target. Each task is wrapped in a dnd-item div that serves as the animation target.

Styling Enhanced Visual Feedback

The visual enhancements need corresponding CSS styles to create the polished appearance. These styles will provide clear feedback during drag operations and smooth animations during reordering.

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, background-color 0.2s ease, box-shadow 0.2s ease;
}

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

.column-highlight {
  background-color: color-mix(in srgb, var(--color-primary) 5%, var(--color-columnBg));
  box-shadow: 0 0 0 2px var(--color-primary), 0 4px 8px rgba(0, 0, 0, 0.1);
}

h2 {
  margin-top: 0;
  font-size: 1.2rem;
  border-bottom: 2px solid var(--color-muted);
  padding-bottom: 0.5rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.count {
  background: var(--color-muted);
  color: var(--color-text);
  border-radius: 9999px;
  padding: 0.1rem 0.5rem;
  font-size: 0.875rem;
}

.tasks {
  flex-grow: 1;
  min-height: 200px; /* Minimum height for dropzone */
  padding-bottom: 1rem;
}

.loading-container {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  color: var(--color-secondary);
  gap: 0.5rem;
}

.dnd-item {
  margin-bottom: 0.5rem;
}

@media (max-width: 950px) {
  .column {
    width: 100%;
  }
}

The column-highlight class uses CSS color mixing to create a subtle background tint and adds a colored border when the column is a valid drop target. The transitions ensure that these visual changes feel smooth and natural.

Updating Drag Store for Enhanced Feedback

We need to update our drag store functions to support the enhanced visual feedback and same-column operations. These updates will provide better column highlighting and handle empty column scenarios.

Update your src/lib/dndStore.svelte.js file with these enhanced functions:

// Handle drag over on column
export function handleDragOverColumn(event, targetStatus) {
  event.preventDefault();
  event.dataTransfer.dropEffect = 'move';
  
  const dropzone = event.currentTarget;
  dropzone.classList.add('drag-over');
  
  // If dragging over empty column, add placeholder
  const tasks = dropzone.querySelectorAll('.task-card');
  if (tasks.length === 0 && !dropzone.querySelector('.drag-placeholder')) {
    const placeholder = createPlaceholder();
    dropzone.appendChild(placeholder);
  }
}

// Handle drag leave
export function handleDragLeave(event) {
  const dropzone = event.currentTarget;
  
  // Only remove drag-over class if we're leaving the dropzone entirely
  const relatedTarget = event.relatedTarget;
  if (!dropzone.contains(relatedTarget)) {
    dropzone.classList.remove('drag-over');
  }
}

These functions provide more sophisticated drag over handling, including proper cleanup when leaving drop zones and automatic placeholder creation for empty columns. The enhanced logic ensures that placeholders appear consistently and that visual feedback is accurate.

Enhanced Drag Cleanup

The cleanup function needs to be more comprehensive to handle the additional elements and states we've introduced with the enhanced system. Let's update the cleanup to handle all the new scenarios.

Update the endDrag function in your src/lib/dndStore.svelte.js file:

// End dragging
export function endDrag() {
  if (dragState.draggedElement) {
    dragState.draggedElement.classList.remove('dragging');
  }
  
  // Remove all placeholders
  document.querySelectorAll('.drag-placeholder').forEach(p => p.remove());
  
  // Remove all drag-over classes
  document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
  
  // Reset state
  dragState.isDragging = false;
  dragState.draggedTask = null;
  dragState.draggedElement = null;
  dragState.sourceColumn = null;
  dragState.placeholder = null;
  dragState.draggedOverTask = null;
}

This enhanced cleanup function removes all temporary visual elements and resets all state properties, ensuring that the interface returns to a clean state regardless of how the drag operation ended.

Creating the Drag Handle Toggle Component

User preferences are essential for creating inclusive interfaces. Let's create a component that allows users to toggle between drag handle mode and full-card dragging, giving them control over their interaction experience.

Create a new file src/components/DragHandleToggle.svelte:

<script>
  import { toggleDragHandleMode, dragState } from '$lib/dndStore.svelte.js';
  
  function toggleHandle() {
    toggleDragHandleMode(!dragState.useDragHandle);
  }
</script>

<div class="drag-toggle">
  <label class="toggle-label">
    <input 
      type="checkbox"
      checked={dragState.useDragHandle}
      onchange={toggleHandle}
      aria-describedby="drag-help-text"
    />
    <span class="toggle-text">Use drag handles</span>
  </label>
  <div class="help-text" id="drag-help-text">
    {#if dragState.useDragHandle}
      Drag tasks using the handle icon on the right. This prevents accidental dragging when clicking to expand tasks.
    {:else}
      Click and drag anywhere on the task to move it. Task expansion is disabled to prevent conflicts.
    {/if}
  </div>
</div>

<style>
  .drag-toggle {
    background-color: var(--color-muted);
    padding: 0.75rem;
    border-radius: 6px;
    margin-bottom: 1rem;
  }
  
  .toggle-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 500;
    cursor: pointer;
  }
  
  .toggle-text {
    color: var(--color-text);
  }
  
  .help-text {
    margin-top: 0.25rem;
    font-size: 0.75rem;
    color: var(--color-secondary);
    line-height: 1.4;
  }
</style>

This component provides a simple toggle interface with contextual help text that changes based on the current mode as seem in the image below. The reactive nature of Svelte's runes means the help text updates automatically when the user changes their preference.

Integrating User Preferences in the Main Page

Now let's integrate the drag handle toggle into the main application page so users can easily access and modify their preferences. We'll position it in a logical location that's accessible but doesn't interfere with the main workflow.

Update src/routes/+page.svelte to include the drag handle toggle:

<script>
  import Board from '../components/Board.svelte';
  import TaskForm from '../components/TaskForm.svelte';
  import StatsBar from '../components/StatsBar.svelte';
  import NotificationManager from '../components/NotificationManager.svelte';
  import DragHandleToggle from '../components/DragHandleToggle.svelte';
  import Header from '../components/Header.svelte';
  import { getTotalTasks } from '$lib/taskStore.svelte.js';
</script>

<svelte:head>
  <title>Kanban Board | My Tasks</title>
</svelte:head>

<div class="container">
  <Header />
  
  <div class="stats-header">
    <StatsBar totalTasks={getTotalTasks()} />
  </div>
  
  <TaskForm />
  <DragHandleToggle />
  <Board />
  <NotificationManager />
</div>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
  }
  
  .stats-header {
    display: flex;
    justify-content: center;
    margin-bottom: 1rem;
  }
</style>

The toggle component is positioned between the task form and the board, making it easily accessible while not interfering with the main workflow. Users can quickly switch between interaction modes and see immediate feedback about how the change affects their experience.

Enhancing Placeholder Visual Design

The placeholder element is a crucial part of the user experience during drag operations. Let's enhance its visual design to make it more informative and better integrated with your design system.

Update the createPlaceholder function in your src/lib/dndStore.svelte.js file:

// 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.backgroundColor = 'color-mix(in srgb, var(--color-primary) 10%, var(--color-cardBg))';
  placeholder.style.border = '1px dashed var(--color-primary)';
  placeholder.style.borderRadius = '6px';
  return placeholder;
}

This enhanced placeholder uses CSS color mixing to create a subtle background that matches your design system while clearly indicating where the dropped task will be positioned. The dashed border provides clear visual distinction from actual task cards.

Integrating our changes in Board.svelte

To complete the integration, we need to ensure the Board component passes the correct props to the columns. Update src/components/Board.svelte:

<script>
  import Column from './Column.svelte';
  import TaskList from './TaskList.svelte';
  import LoadingIndicator from './LoadingIndicator.svelte';
  import EmptyState from './EmptyState.svelte';
  import { 
    getTodoTasks, 
    getInProgressTasks, 
    getDoneTasks,
    loadTasks
  } from '$lib/taskStore.svelte.js';
  import { loadingStates } from '$lib/loadingStore.svelte.js';
  
  // Check if all columns are empty
  const isAllEmpty = $derived(
    getTodoTasks().length === 0 && 
    getInProgressTasks().length === 0 && 
    getDoneTasks().length === 0
  );
  
  async function retryLoading() {
    await loadTasks();
  }
</script>

{#snippet columnTasks(tasks, status)}
  <TaskList {tasks} {status} emptyType={status} />
{/snippet}

{#if loadingStates.global}
  <div class="board-loading">
    <LoadingIndicator type="global" size="large" />
    <p>Loading your board...</p>
  </div>
{:else if isAllEmpty}
  <div class="empty-board">
    <EmptyState type="tasks" />
  </div>
{:else}
  <div class="board">
    <Column title="To Do" tasks={getTodoTasks()} status="todo" taskCount={getTodoTasks().length} />
    <Column title="In Progress" tasks={getInProgressTasks()} status="inprogress" taskCount={getInProgressTasks().length} />
    <Column title="Done" tasks={getDoneTasks()} status="done" taskCount={getDoneTasks().length} />
  </div>
{/if}

<style>
  .board {
    display: flex;
    gap: 1rem;
    justify-content: center;
  }
  
  .board-loading,
  .empty-board {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 400px;
    text-align: center;
  }
  
  .board-loading p {
    margin-top: 1rem;
    color: var(--color-secondary);
  }
  
  @media (max-width: 950px) {
    .board {
      flex-direction: column;
    }
  }
</style>

With all these components integrated, your enhanced drag and drop system is now complete. Users can toggle between drag handle mode and full-card dragging, reorder tasks within columns with smooth animations, and enjoy enhanced visual feedback throughout their interactions. The system maintains all the functionality from Unit 1 while adding the professional polish that makes the interface feel modern and responsive.

Adding Preference Persistence

To create a truly user-friendly experience, we should save user preferences so they don't have to reconfigure their settings every time they visit the application. Let's add localStorage persistence for the drag handle preference.

Update the drag state initialization and toggle function in src/lib/dndStore.svelte.js:

// Track drag state with persistent preferences
export const dragState = $state({
  isDragging: false,
  draggedTask: null,
  draggedElement: null,
  sourceColumn: null,
  placeholder: null,
  draggedOverTask: null,
  useDragHandle: typeof localStorage !== 'undefined' 
    ? localStorage.getItem('kanban-drag-handles') !== 'false' 
    : true
});

// Toggle drag handle setting with persistence
export function toggleDragHandleMode(useHandle) {
  dragState.useDragHandle = useHandle;
  
  // Persist the preference
  if (typeof localStorage !== 'undefined') {
    localStorage.setItem('kanban-drag-handles', useHandle.toString());
  }
}

This approach loads the user's preference from localStorage when the application starts and saves any changes immediately. The default value is true (use drag handles) unless the user has explicitly disabled them. The typeof localStorage !== 'undefined' check ensures the code works even in environments where localStorage is not available.

Accessibility concerns

We should also consider adding keyboard support for users who prefer or require keyboard navigation. Let's enhance the TaskCard component to support keyboard-initiated drag operations:

<script>
  import { slide } from 'svelte/transition';
  import TaskActions from './TaskActions.svelte';
  import DeleteButton from './DeleteButton.svelte';
  import { startDrag, endDrag, handleDragOverTask } from '$lib/dndStore.svelte.js';
  
  let { id, title, description = '', status, useHandle = true } = $props();
  let isExpanded = $state(false);
  
  function toggleExpand(e) {
    // Do not expand when clicking on drag handle
    if (useHandle && e.target.closest('.drag-handle')) {
      return;
    }
    isExpanded = !isExpanded;
  }
  
  function handleDragStart(event) {
    // Check if dragging from handle when handles are enabled
    if (useHandle && !event.target.closest('.drag-handle')) {
      event.preventDefault();
      return;
    }
    
    const task = { id, title, description };
    startDrag(event, task, status);
  }
  
  function handleDragEnd(event) {
    endDrag();
  }
  
  function handleDragOver(event) {
    const task = { id, title, description };
    handleDragOverTask(event, task, status);
  }
  
  function handleKeyDown(event) {
    // Space or Enter to start keyboard drag mode
    if ((event.code === 'Space' || event.code === 'Enter') && useHandle) {
      event.preventDefault();
      // This would trigger a keyboard-accessible drag mode
      // Implementation would depend on your accessibility requirements
    }
  }
</script>

The keyboard support provides an alternative interaction method for users who cannot or prefer not to use mouse-based dragging. This is particularly important for accessibility compliance and inclusive design.

Summary and Practice Preparation

Congratulations! You've successfully transformed your basic drag and drop system into a sophisticated, professional-grade interaction system. Your Kanban board now features drag handles for precise control, same-column task reordering with smooth animations, enhanced visual feedback, and user preference controls that make the interface adaptable to different user needs.

Let's review the key enhancements you've implemented in this lesson. You added drag handles that separate drag operations from other card interactions, eliminating the conflict between dragging and expanding tasks. You implemented same-column reordering functionality that allows users to prioritize tasks within their current status, complete with precise positioning based on mouse location. You enhanced the visual feedback system with column highlighting, smooth flip animations, and improved placeholder positioning that makes drag operations feel fluid and predictable.

The user preference system you built demonstrates thoughtful interface design. By providing a toggle between drag handle mode and full-card dragging, you've created an interface that can accommodate different user preferences and accessibility needs. The persistent storage of these preferences ensures that users don't have to reconfigure their settings each time they use the application.

The technical implementation showcases several important Svelte concepts. You used Svelte's $state rune to create reactive state that automatically updates the interface when preferences change. You leveraged the built-in flip animation to create smooth reordering transitions without complex animation code. You implemented conditional rendering to show or hide drag handles based on user preferences, and you used CSS color mixing to create sophisticated visual feedback that integrates seamlessly with your existing design system.

These enhancements significantly improve the user experience of your Kanban board. The interface now feels responsive, polished, and professional. Users can organize their tasks with precision and confidence, knowing that the interface will respond predictably to their actions. The visual feedback provides clear communication about what's happening during drag operations, reducing cognitive load and making the interface more intuitive.

In the practice exercises that follow this lesson, you'll have the opportunity to implement these enhanced drag interactions step by step. You'll start by adding drag handles to your existing task cards, then implement same-column reordering, and finally add the visual enhancements and user preference controls. These exercises will reinforce the concepts covered in this lesson while giving you hands-on experience with the implementation details.

The next unit in this course will introduce SvelteKit routing, which will allow you to create multiple views of your tasks and implement deep linking to individual task details. The enhanced drag interactions you've built in this lesson will integrate seamlessly with the routing features, creating a comprehensive task management application that rivals professional productivity tools. The foundation you've established here will support even more advanced features as you continue building your Kanban application.

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