Skip to content

The Edge of the Cyber World See the latest

Apps

React vs Vue Drag-and-Drop: dnd-kit in 12 Steps [2026]

Drag-and-drop interfaces are everywhere in 2026 — Kanban boards, dashboard builders, file managers, form designers. But the library you pick to power that interaction looks very different depending on whether you’re shipping in React or Vue. React’s ecosystem has quietly consolidated around @dnd-kit after Atlassian stopped actively maintaining react-beautiful-dnd, while Vue developers still lean on SortableJS wrappers like vue-draggable-next and the newer vue-draggable-plus. This tutorial builds the same sortable, drag-and-drop Kanban board twice — once in React 19 with dnd-kit, once in Vue 3.5 with vue-draggable-plus — so you can see exactly where the two approaches diverge in code, bundle size, and accessibility.

By the end you’ll have two working boards, a side-by-side performance comparison, and a clear answer to which library fits your stack. We’ll also cover the pitfalls that trip up most developers the first time they wire up drag-and-drop: stale closures, missing keyboard sensors, reactivity edge cases in nested arrays, and SSR hydration mismatches.

Why Drag-and-Drop Libraries Diverged Between React and Vue

React and Vue solve drag-and-drop differently because their reactivity models are fundamentally different. React re-renders a component tree in response to state changes and relies on a reconciliation pass, so a drag-and-drop library built for React has to work around concurrent rendering, batched updates, and (as of React 19) features like useTransition and useOptimistic. Vue’s reactivity system tracks dependencies at the property level through proxies, which is why a thin wrapper around the vanilla JavaScript library SortableJS can slot in with far less custom plumbing.

That architectural difference shows up directly in package choice. react-beautiful-dnd, once the default recommendation for React teams, is widely flagged in 2026 comparison guides as deprecated or effectively unmaintained — it still pulls a meaningful chunk of weekly npm downloads from legacy codebases, but new projects are steered toward @dnd-kit, which ships full React 19 support, a modular “headless” API, and a core bundle small enough that most teams don’t think twice about adding it. On the Vue side, there’s no direct dnd-kit equivalent; the ecosystem instead orbits SortableJS wrappers, with vuedraggable (Vue 2), vue-draggable-next (Vue 3), and the actively maintained vue-draggable-plus covering most production use cases.

This guide assumes you already know basic React and Vue component syntax. If you haven’t set up either framework’s build tooling yet, our React vs Vue TypeScript setup guide covers that groundwork first.

It’s worth understanding the underlying mental model each library asks you to adopt, because it shapes every decision that follows. dnd-kit treats drag-and-drop as a set of composable primitives: sensors detect input, a collision-detection algorithm decides what’s “over” what, and your application code owns the actual state mutation. Nothing in dnd-kit touches the DOM directly outside of applying transform styles — it hands you coordinates and lets you render however you want. That’s why teams building custom Kanban boards, nested drag targets, or drag-to-resize panels gravitate toward it: the library doesn’t assume a particular list shape.

SortableJS, and by extension vue-draggable-plus, takes the opposite approach. It directly manipulates the DOM order during a drag, then syncs that order back into your bound array once the drag ends. That’s a faster path to a working sortable list, but it means the library is making assumptions about your markup structure that dnd-kit deliberately avoids. For a straightforward Kanban board like the one in this tutorial, both approaches land in roughly the same place. For more exotic interactions — drag-to-reorder inside a virtualized list, or drag targets that span multiple scroll containers — the difference in philosophy becomes the deciding factor.

Prerequisites and Versions

Confirm these versions before you start. Drag-and-drop libraries are sensitive to framework version mismatches, and an outdated peer dependency is the single most common setup failure reported in GitHub issues for both libraries.

Tool Minimum Version Recommended Version Notes
Node.js 18.x 22.x LTS Required for Vite 6 and the React 19 toolchain
React 18.0 19.x dnd-kit v6 targets React 19’s concurrent rendering model
@dnd-kit/core 6.0 latest 6.x Core engine; roughly 6 KB gzipped on its own
@dnd-kit/sortable 8.0 latest 8.x Sortable list/grid preset built on dnd-kit/core
Vue 3.4 3.5.x Vue 3.5 reworked reactivity tracking for large arrays
vue-draggable-plus 0.4 latest Actively maintained Vue 3 wrapper around SortableJS
Vite 5.x 6.x Build tool for both project scaffolds in this tutorial
TypeScript 5.4 5.6+ Optional but used throughout this tutorial’s code samples

You’ll also need a code editor with TypeScript support (VS Code or a JetBrains IDE both work fine), a terminal, and roughly 90 minutes if you’re building both boards end to end. If you only need one framework, budget about 45 minutes.

Step 1: Scaffold the React Project With Vite

Start with a clean Vite + React + TypeScript scaffold. Vite’s dev server starts in well under a second on modern hardware and gives you fast HMR while you iterate on drag interactions, which matters because you’ll be reloading constantly while tuning sensor thresholds.

npm create vite@latest react-dnd-board -- --template react-ts
cd react-dnd-board
npm install
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
npm run dev

You should see output confirming the dev server is running, typically on http://localhost:5173. If npm resolves peer dependency warnings for React 19, that’s expected the first time you install dnd-kit against a brand-new major React release — check the package’s changelog on GitHub if a warning turns into a hard install failure.

Step 2: Scaffold the Vue Project With Vite

Repeat the same process for Vue, using the official Vue 3 + TypeScript template so both projects start from an equivalent baseline.

npm create vite@latest vue-dnd-board -- --template vue-ts
cd vue-dnd-board
npm install
npm install vue-draggable-plus
npm run dev

Note how much lighter this install is — one package instead of three. That’s a direct consequence of vue-draggable-plus being a thin wrapper around SortableJS rather than a ground-up reimplementation, which is also why its API surface is smaller than dnd-kit’s.

Step 3: Define the Shared Data Model

Both boards will use the same data shape: a list of columns, each holding an array of cards. Keeping the model identical across both implementations makes the comparison fair and makes it easier to port your own app’s data later.

// shared/types.ts
export interface Card {
  id: string;
  title: string;
  tag: 'bug' | 'feature' | 'chore';
}

export interface Column {
  id: string;
  title: string;
  cards: Card[];
}

export const initialColumns: Column[] = [
  {
    id: 'todo',
    title: 'To Do',
    cards: [
      { id: 'c1', title: 'Fix login redirect loop', tag: 'bug' },
      { id: 'c2', title: 'Add dark mode toggle', tag: 'feature' },
    ],
  },
  {
    id: 'in-progress',
    title: 'In Progress',
    cards: [
      { id: 'c3', title: 'Refactor auth middleware', tag: 'chore' },
    ],
  },
  {
    id: 'done',
    title: 'Done',
    cards: [
      { id: 'c4', title: 'Ship onboarding flow', tag: 'feature' },
    ],
  },
];

This structure mirrors what most production Kanban tools actually store: a normalized list of columns, each owning an ordered array of card IDs or objects. If your real app fetches this from a database, treat this file as the shape your API response should already match before it hits your component tree.

Step 4: Build the React Board With dnd-kit

dnd-kit’s core primitive is the DndContext, which wraps everything that can participate in a drag interaction. Inside it, SortableContext defines a group of sortable items, and each item registers itself with the useSortable hook. This is the “headless” part of dnd-kit’s design: it gives you drag state and event handlers, but you own all the rendering and styling.

// src/Board.tsx
import { useState } from 'react';
import {
  DndContext,
  closestCenter,
  PointerSensor,
  KeyboardSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from '@dnd-kit/core';
import {
  SortableContext,
  arrayMove,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { initialColumns, type Column } from '../shared/types';
import { SortableCard } from './SortableCard';

export function Board() {
  const [columns, setColumns] = useState(initialColumns);

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
  );

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    if (!over || active.id === over.id) return;

    setColumns((prev) =>
      prev.map((col) => {
        const oldIndex = col.cards.findIndex((c) => c.id === active.id);
        const newIndex = col.cards.findIndex((c) => c.id === over.id);
        if (oldIndex === -1 || newIndex === -1) return col;
        return { ...col, cards: arrayMove(col.cards, oldIndex, newIndex) };
      })
    );
  }

  return (
    
      
{columns.map((col) => (

{col.title}

c.id)} strategy={verticalListSortingStrategy}> {col.cards.map((card) => ( ))}
))}
); }

Note the KeyboardSensor registration — that’s not optional decoration. dnd-kit is described in its own docs and in third-party comparisons as accessible by default specifically because keyboard and screen-reader support ships in the core sensor set rather than being bolted on later. Skipping it means keyboard-only users can’t reorder anything.

Step 5: Build the Sortable Card Component in React

Each card needs to call useSortable to get its drag handlers, transform, and transition values, then apply them to a DOM node via inline styles.

// src/SortableCard.tsx
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { Card } from '../shared/types';

export function SortableCard({ card }: { card: Card }) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
    useSortable({ id: card.id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
  };

  return (
    
{card.title}
); }

The CSS.Transform.toString() helper from @dnd-kit/utilities converts dnd-kit’s internal transform object into a valid CSS string, avoiding a common mistake where developers manually string-template the transform and get the syntax subtly wrong.

Step 6: Build the Vue Board With vue-draggable-plus

Vue’s approach collapses most of the previous two steps into a single component. Because vue-draggable-plus wraps SortableJS, you bind the list directly with a two-way v-model-style prop, and the library handles DOM reordering and array mutation for you.

<!-- src/Board.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';
import { initialColumns, type Column } from '../shared/types';

const columns = ref<Column[]>(initialColumns);
</script>

<template>
  <div class="board">
    <div v-for="col in columns" :key="col.id" class="column">
      <h3>{{ col.title }}</h3>
      <VueDraggable
        v-model="col.cards"
        :animation="150"
        group="cards"
        item-key="id"
        class="card-list"
      >
        <template #item="{ element }">
          <div :class="`card card--${element.tag}`">{{ element.title }}</div>
        </template>
      </VueDraggable>
    </div>
  </div>
</template>

Compare the line count: the Vue version needs no separate sortable-item component, no manual arrayMove call, and no explicit sensor configuration for mouse or touch input. The tradeoff is that you’re relying on vue-draggable-plus’s defaults for collision detection and drag thresholds, which are less configurable than dnd-kit’s pluggable sensor system.

Step 7: Enable Cross-Column Dragging

A single sortable list is only half the Kanban experience — cards need to move between columns too. In dnd-kit, this means tracking which column each card currently belongs to and updating both the source and destination arrays in onDragEnd.

function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over) return;

  const activeColumn = columns.find((c) => c.cards.some((card) => card.id === active.id));
  const overColumn = columns.find(
    (c) => c.id === over.id || c.cards.some((card) => card.id === over.id)
  );
  if (!activeColumn || !overColumn || activeColumn.id === overColumn.id) return;

  setColumns((prev) => {
    const activeCard = activeColumn.cards.find((c) => c.id === active.id)!;
    return prev.map((col) => {
      if (col.id === activeColumn.id) {
        return { ...col, cards: col.cards.filter((c) => c.id !== active.id) };
      }
      if (col.id === overColumn.id) {
        return { ...col, cards: [...col.cards, activeCard] };
      }
      return col;
    });
  });
}

In Vue, cross-column drag is dramatically simpler: set the same group value on every VueDraggable instance in step 6, and SortableJS handles moving items between the underlying arrays automatically. That single-line change is the clearest illustration of the composability-versus-convenience tradeoff between the two libraries.

Step 8: Add a Drag Overlay for Visual Polish

Without an overlay, the dragged card in dnd-kit can look visually cramped inside its original list position while dragging. The DragOverlay component renders a floating copy of the active item that follows the pointer, independent of document flow.

import { DragOverlay, type DragStartEvent } from '@dnd-kit/core';

const [activeCard, setActiveCard] = useState<Card | null>(null);

function handleDragStart(event: DragStartEvent) {
  const card = columns.flatMap((c) => c.cards).find((c) => c.id === event.active.id);
  setActiveCard(card ?? null);
}

// inside <DndContext>, after the columns map:
<DragOverlay>
  {activeCard ? <div className="card card--dragging">{activeCard.title}</div> : null}
</DragOverlay>

Vue-draggable-plus achieves the same floating effect automatically through SortableJS’s ghost element behavior, so you typically only need to style the .sortable-ghost and .sortable-drag CSS classes it applies rather than manage a separate overlay component.

Step 9: Style Both Boards Consistently

Use identical CSS for both projects so any performance or feel differences you notice come from the library, not the styling. A simple flex-based board works for this comparison.

.board { display: flex; gap: 16px; padding: 24px; }
.column { background: #f4f5f7; border-radius: 8px; padding: 12px; width: 280px; min-height: 400px; }
.card { background: white; border-radius: 6px; padding: 10px 12px; margin-bottom: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); cursor: grab; }
.card--bug { border-left: 3px solid #e5484d; }
.card--feature { border-left: 3px solid #30a46c; }
.card--chore { border-left: 3px solid #f76b15; }
.card--dragging { box-shadow: 0 8px 16px rgba(0,0,0,0.2); }

Both frameworks accept plain CSS the same way, so there’s nothing framework-specific here — just make sure Vue’s scoped styles (<style scoped>) or React’s CSS Modules don’t accidentally strip the class names the drag libraries apply dynamically, like sortable-ghost.

Step 10: Persist Reordering to a Backend

A drag-and-drop board that resets on refresh isn’t production-ready. Debounce the persistence call so you’re not firing a network request on every pixel of movement — only after the drag settles.

// React: call this at the end of handleDragEnd, after setColumns
async function persistOrder(updatedColumns: Column[]) {
  await fetch('/api/board/reorder', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ columns: updatedColumns }),
  });
}

In Vue, watch the columns ref with a debounced handler instead, since vue-draggable-plus mutates the bound array directly rather than exposing a discrete “drag ended” callback by default (though it does emit an end event you can also hook into for the same effect). If you’re storing board state in Postgres or a similar relational store, our React vs Vue data fetching guide covers wiring TanStack Query and Pinia Colada into exactly this kind of mutation flow.

Step 11: Test the Drag Interactions

Drag-and-drop is notoriously hard to unit test because it depends on pointer events that jsdom doesn’t fully simulate. For React, dnd-kit’s own test utilities let you dispatch synthetic drag events; for Vue, most teams fall back to end-to-end tools like Playwright for real pointer simulation. If you already run Vitest or Jest in these projects, our React vs Vue testing comparison walks through configuring both for component-level coverage before you add drag-specific end-to-end tests on top.

// Playwright example — works for both React and Vue boards
import { test, expect } from '@playwright/test';

test('drags a card between columns', async ({ page }) => {
  await page.goto('http://localhost:5173');
  const card = page.locator('.card', { hasText: 'Fix login redirect loop' });
  const target = page.locator('.column', { hasText: 'In Progress' });
  await card.dragTo(target);
  await expect(target).toContainText('Fix login redirect loop');
});

Run this against both dev servers to confirm cross-column dragging works identically in each implementation before you ship.

Step 12: Compare Bundle Size, Downloads, and Accessibility

With both boards running, the practical differences come down to three things: how much JavaScript you ship, how actively the library is maintained, and how much accessibility work is done for you out of the box.

Metric React (@dnd-kit) Vue (vue-draggable-plus)
Core package weekly npm downloads ~2.8M (@dnd-kit/core) Far smaller; SortableJS itself is widely reused across ecosystems
Approx. core bundle size ~6 KB gzipped (core only) Depends on bundled SortableJS; typically larger due to the underlying vanilla-JS library
Architecture Headless, composable hooks + primitives Declarative wrapper around SortableJS DOM manipulation
Keyboard accessibility Built-in via KeyboardSensor Not built-in; requires custom implementation
Legacy alternative still in use react-beautiful-dnd (~1.2M weekly downloads, deprecated) vuedraggable (Vue 2 only, in maintenance mode)
Framework version targeted React 19 (concurrent-safe) Vue 3.5 (optimized reactivity for large arrays)

The accessibility gap is the most consequential line in that table for teams with compliance requirements. dnd-kit ships keyboard sensor support as a first-class citizen, which is why 2026 comparison write-ups consistently describe it as “accessible by default.” SortableJS-based wrappers, including vue-draggable-plus, don’t include equivalent keyboard reordering out of the box — if you need it in a Vue app, you’ll be writing custom keydown handlers that call the array-mutation logic manually, essentially reimplementing what dnd-kit’s KeyboardSensor already provides.

On raw list performance, Vue 3.5’s reactivity refactor matters more than it might seem. Vue 3.5 reworked how the reactivity system tracks large, deeply nested reactive arrays, which directly benefits drag-and-drop boards with hundreds of cards since every reorder previously had to re-evaluate more dependency tracking than necessary. React’s story here is different: React 19’s concurrent rendering features like useTransition let you mark a large reorder as non-urgent so the UI stays responsive, but you have to opt into that manually rather than getting it as a reactivity-system default.

Migrating an Existing react-beautiful-dnd App to dnd-kit

If you’re maintaining an older React codebase, chances are it still has react-beautiful-dnd wired into a board or list somewhere. The migration to dnd-kit isn’t a drop-in swap, but the conceptual mapping is close enough that most teams complete it component by component rather than as a full rewrite.

Start by identifying every DragDropContext, Droppable, and Draggable in your codebase — those map roughly to dnd-kit’s DndContext, SortableContext, and useSortable respectively, but the callback signatures differ. react-beautiful-dnd’s onDragEnd hands you a DropResult object with source and destination index positions; dnd-kit’s onDragEnd hands you active and over objects referencing IDs, not indices. That’s a meaningful mental shift: you’ll be rewriting reorder logic around ID lookups instead of array index math, which is actually less error-prone once you’re used to it, since index-based logic is where most react-beautiful-dnd bugs originated in the first place.

// Before: react-beautiful-dnd
function onDragEnd(result: DropResult) {
  if (!result.destination) return;
  const items = Array.from(cards);
  const [moved] = items.splice(result.source.index, 1);
  items.splice(result.destination.index, 0, moved);
  setCards(items);
}

// After: dnd-kit
function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over || active.id === over.id) return;
  setCards((items) => {
    const oldIndex = items.findIndex((i) => i.id === active.id);
    const newIndex = items.findIndex((i) => i.id === over.id);
    return arrayMove(items, oldIndex, newIndex);
  });
}

Budget extra QA time for any board that relied on react-beautiful-dnd’s automatic scroll-while-dragging behavior near viewport edges — dnd-kit doesn’t include this out of the box and you’ll need to add a scroll-container sensor or a small custom hook to replicate it. Teams migrating production boards with hundreds of daily active users typically report the rewrite taking one to three days per board, depending on how much custom styling was coupled to react-beautiful-dnd’s specific DOM output.

Benchmarking Drag Performance on Large Lists

The Kanban board in this tutorial only has a handful of cards, which won’t reveal any performance difference between the two libraries. To actually compare how they behave under load, seed each board with 500 cards spread across the three columns and watch React DevTools Profiler (for the React board) or Vue DevTools’ performance tab (for the Vue board) while dragging.

// Quick seed generator — paste into either project's initial state
function generateCards(count: number): Card[] {
  const tags: Card['tag'][] = ['bug', 'feature', 'chore'];
  return Array.from({ length: count }, (_, i) => ({
    id: `card-${i}`,
    title: `Task item ${i + 1}`,
    tag: tags[i % 3],
  }));
}

On the React side, watch for re-render count during a single drag gesture. Because dnd-kit only re-renders the components whose sort position actually changed (when using the sortable preset’s default strategy), a well-structured board shouldn’t re-render all 500 cards on every pointer move — if it does, that’s usually a sign a parent component further up the tree is re-rendering unnecessarily and dragging every child down with it via prop changes. On the Vue side, Vue 3.5’s finer-grained reactivity tracking means individual card components typically don’t re-render at all during a drag; only the DOM nodes SortableJS physically moves are affected, since Vue’s virtual DOM diffing isn’t in the critical path for a pure DOM reorder.

If you notice jank specifically on drag start rather than during the drag itself, check whether you’re computing anything expensive — like filtering or sorting the full card list — inside the render path instead of memoizing it. This is a far more common cause of perceived “the library is slow” complaints than an actual limitation in either dnd-kit or vue-draggable-plus.

Common Pitfalls When Building Drag-and-Drop Boards

These are the mistakes that show up most often in GitHub issues and Stack Overflow threads for both libraries.

  • Forgetting the KeyboardSensor in dnd-kit. Without it, your board looks fully functional in manual testing but fails any keyboard-only accessibility audit. Always register both PointerSensor and KeyboardSensor together.
  • Mutating state directly inside onDragEnd in React. dnd-kit expects you to treat the columns array as immutable and return a new array from your updater function; direct mutation silently breaks React’s re-render detection.
  • Using array index instead of a stable ID as the sortable key. Both libraries need a persistent unique identifier per item — reusing array index as the key causes cards to visually “jump” to the wrong position mid-drag.
  • Forgetting to set a shared group prop across VueDraggable instances. Without matching group names, cards will bounce back to their origin column instead of transferring.
  • Firing a persistence API call on every drag-over event. This floods your backend with requests. Only persist once in onDragEnd (React) or the end event (Vue), never during intermediate drag movement.
  • Skipping the activation distance constraint on PointerSensor. Without a minimum drag distance, ordinary clicks on cards can accidentally trigger drag mode, breaking click-to-open behavior.
  • Not testing touch devices separately. Both libraries support touch, but default activation thresholds tuned for mouse input often feel too sensitive or too sluggish on mobile — test on an actual device, not just a resized browser window.
  • Assuming SSR “just works.” If you’re server-rendering either board (Next.js or Nuxt), drag state must never be part of the initial server-rendered HTML — hydrate it client-side only, or you’ll get a hydration mismatch warning.

Complete Working Project Structure

Once you’ve worked through all 12 steps, your two project folders should look like this. Keeping the file structure this flat makes it easy to diff the two implementations side by side, which is genuinely useful the first time you’re deciding which library fits a new feature.

react-dnd-board/
├── src/
│   ├── Board.tsx
│   ├── SortableCard.tsx
│   ├── main.tsx
│   └── styles.css
├── shared/
│   └── types.ts
├── package.json
└── vite.config.ts

vue-dnd-board/
├── src/
│   ├── Board.vue
│   ├── main.ts
│   └── styles.css
├── shared/
│   └── types.ts
├── package.json
└── vite.config.ts

Notice the Vue project has one fewer component file than React — the entire sortable-item concept that required a dedicated SortableCard.tsx component in React is handled inline through vue-draggable-plus’s scoped slot in step 6. That’s not a criticism of either approach; it’s simply where each library’s abstraction boundary sits. dnd-kit externalizes more decisions to your code so you can customize deeply. vue-draggable-plus internalizes more decisions so you can ship faster.

If you’re deploying either board to production, wire up your CI pipeline before merging — our React vs Vue deployment pipeline guide covers the build and release steps for both Vite-based projects, including environment variable handling for the persistence endpoint referenced in step 10.

Expected Output

After completing all 12 steps, both dev servers should render a three-column Kanban board where cards can be reordered within a column and dragged across columns, with a visible drop-shadow overlay during the drag and immediate persistence to your backend once you release the card. Your terminal output for the React project should look roughly like this after a clean install:

added 187 packages in 4s
VITE v6.0.0  ready in 312 ms
➜  Local:   http://localhost:5173/
➜  Network: use --host to expose

The Vue project’s install should be noticeably smaller given the single extra dependency:

added 96 packages in 3s
VITE v6.0.0  ready in 268 ms
➜  Local:   http://localhost:5173/
➜  Network: use --host to expose

Troubleshooting Guide

If your board doesn’t behave as expected, check these common failure modes in order.

  • Cards won’t drag at all in React. Confirm every draggable element calls useSortable and spreads both listeners and attributes onto the same DOM node — missing listeners is the most common cause.
  • Cards snap back to their original position in Vue. Check that item-key matches an actual unique property on each object; a missing or duplicate key breaks SortableJS’s internal DOM diffing.
  • “Cannot read properties of undefined” during drag in React. This usually means active.id or over.id doesn’t match any card in your columns array — verify your IDs are strings and consistent across renders.
  • Keyboard dragging does nothing. Make sure focus is actually on the sortable card (tab to it first) and that KeyboardSensor is included in your useSensors array, not just PointerSensor.
  • Drag works on desktop but not on mobile Safari. Add touch-action: none to your card CSS; without it, the browser’s native scroll gesture competes with the drag gesture.
  • Vue board flickers during cross-column drag. This typically means two VueDraggable instances have different :animation values — keep animation duration consistent across all lists in the same group.
  • Persisted order doesn’t match visual order after refresh. Confirm you’re sending the full reordered array to your API, not just the moved item’s new index — partial updates are a common source of drift.
  • TypeScript errors on useSortable’s return type. Update @dnd-kit/core and @dnd-kit/sortable together; mismatched minor versions between the two packages is a frequent cause of type conflicts.
  • SSR hydration warning in Next.js or Nuxt. Wrap the board component in a client-only boundary (dynamic import with ssr: false in Next.js, or ClientOnly in Nuxt) since drag state should never render on the server.

Advanced Tips for Production Boards

Once the basic board works, a few refinements separate a demo from something you’d actually ship. For large boards with hundreds of cards, virtualize the card list in React using a library like TanStack Virtual alongside dnd-kit — rendering every card in the DOM at once will slow down both drag responsiveness and initial paint. dnd-kit’s modular architecture supports this combination directly since it doesn’t assume anything about how your list is rendered.

For Vue, vue-draggable-plus doesn’t have a first-party virtualization story, so large lists are better served by pagination or a “load more” pattern within each column rather than true windowing. If you need optimistic UI updates so the drag feels instant even before your backend confirms the reorder, wrap your persistence call in the same optimistic-update pattern used for other mutations — our React vs Vue state management guide covers implementing that pattern in Redux and Pinia respectively, which pairs directly with the persistence step above.

Finally, if your board needs nested drag targets — for example, dragging a card into a sub-checklist inside another card — dnd-kit’s collision detection strategies (closestCenter, closestCorners, rectIntersection) each behave differently with nested droppables, and it’s worth testing all three against your actual layout rather than assuming the default works. Vue-draggable-plus handles nested lists by simply nesting additional VueDraggable components, but be aware that deeply nested SortableJS instances can develop event-bubbling conflicts if you don’t set group carefully at each nesting level.

Which Library Should You Choose?

If you’re building a new React 19 application, @dnd-kit is the clear default in 2026 — it’s the library the React community has consolidated around, it’s actively maintained, it’s small, and it handles accessibility without extra work. Avoid starting a new project on react-beautiful-dnd; even though it still shows meaningful download numbers from legacy codebases, comparison guides across the ecosystem consistently flag it as unmaintained and warn that internal APIs like findDOMNode() that it relies on have been removed in newer React releases.

For Vue 3.5 projects, vue-draggable-plus is the pragmatic choice for typical CRUD boards, sortable lists, and simple Kanban interfaces — it gets you a working implementation with a fraction of the code shown for dnd-kit in this tutorial. Where it falls short is accessibility and highly custom interactions; if your Vue app needs keyboard-accessible drag-and-drop for compliance reasons, budget extra time to build that layer yourself, since it isn’t included by default the way it is in dnd-kit.

Reference documentation for both libraries is worth bookmarking as you build: the official dnd-kit documentation, the dnd-kit GitHub repository, and the Vue Draggable GitHub repository all stay current with each new release. For framework fundamentals behind these examples, the Vue.js reactivity documentation and the official React 19 release notes are the primary sources this tutorial draws its version details from.

Decision Matrix: Picking a Library by Use Case

Use this table as a quick reference when scoping a new feature rather than defaulting to whatever your team already knows.

Use Case Best Fit Why
Simple sortable to-do list Either Both handle single-column reordering with minimal code
Multi-column Kanban board Either Cross-column drag is a config option in Vue, a few extra lines in React
Keyboard-accessible drag (compliance requirement) dnd-kit KeyboardSensor ships built-in; SortableJS wrappers need custom keydown handling
Dashboard builder with resizable, nested panels dnd-kit Headless architecture supports custom collision detection and nested droppables
Fast internal tool, tight deadline vue-draggable-plus Fewer lines of code to a working implementation
Large list (500+ items) with frequent reorders Vue + vue-draggable-plus Vue 3.5’s reactivity refactor reduces tracking overhead on large arrays
Virtualized list with drag-and-drop dnd-kit Composable enough to pair with TanStack Virtual; no equivalent Vue pattern is well established

Frequently Asked Questions

Is react-beautiful-dnd still usable in 2026?
It still runs in older codebases and continues to see real weekly downloads, but it’s widely documented as deprecated. New projects should use @dnd-kit instead, since react-beautiful-dnd depends on internal React APIs that are being phased out in newer releases.

Does @dnd-kit work with React 19’s concurrent features?
Yes. dnd-kit’s v6 release added explicit support for React 19’s concurrent rendering model, including compatibility with hooks like useTransition when you want to mark large reorders as non-blocking updates.

What’s the difference between vue-draggable-next and vue-draggable-plus?
vue-draggable-next was the original Vue 3 port of the Vue 2 vuedraggable library. vue-draggable-plus is a newer, actively maintained rewrite with better TypeScript support and a more modern component API, which is why it’s used in this tutorial.

Can I use dnd-kit and vue-draggable-plus together in a hybrid app?
Not directly in the same component tree, since they’re built for different frameworks. If you’re running a micro-frontend architecture with both React and Vue islands, each island manages its own drag-and-drop independently using its native library.

Does either library support touch screens out of the box?
Both do. dnd-kit includes a TouchSensor alongside PointerSensor and KeyboardSensor, while vue-draggable-plus inherits SortableJS’s native touch support. Test on real devices, since default activation thresholds are tuned for mouse input first.

How much does dnd-kit add to my bundle size?
The core package is roughly 6 KB gzipped on its own; adding the sortable preset and utilities package brings the total higher but still stays in the range most teams consider negligible next to a typical React application bundle.

Do I need a state management library like Redux or Pinia for drag-and-drop?
Not necessarily. Both examples in this tutorial use local component state (useState in React, ref in Vue), which is sufficient for a single board. If multiple components across your app need to react to reordering, lifting that state into Redux or Pinia makes sense — see our state management comparison for the tradeoffs.

Why does my Vue board’s drag animation look choppy compared to React’s?
This is usually a mismatched :animation prop across VueDraggable instances, or CSS transitions on the card element competing with SortableJS’s own transform animation. Keep the animation duration consistent everywhere and avoid adding your own transition: all rule to draggable elements.

Related Coverage

Source: Tech Insider