How to Automate Drag and Drop in Selenium and Playwright
Practice drag-and-drop automation — simple drop zones, sortable lists, kanban column transfers, type-restricted zones, and multi-column boards in Playwright, Selenium & Cypress.
Interactive Scenarios
Drag the item into the drop zone.
Drag each card to its matching zone. Cards show which zone they belong to.
→ zone-a
→ zone-b
→ zone-c
Drag items to reorder the list. Each item has data-item-id — use it as the locator anchor.
- Playwright
- Cypress
- Selenium
- WebdriverIO
- Puppeteer
Column headings have no data-testid. Scope via data-column-id then find data-task-id.
Todo3
Done1
Zones only accept their matching shape type. Rejection feedback has no data-testid — locate via role="status".
Some cards have no data-testid. Find them via aria-label or getByRole('article',{ name: /Fix login/ }) .
| ID | Scenario | Type | Priority | |
|---|---|---|---|---|
| DD_001 | Drag a single item to a drop zone Expected: Item lands in the zone; result text confirms the drop. ✅ positivehigh | positive | high | |
| DD_002 | Drag item using locator.dragTo() API Expected: Same result as dragAndDrop — item lands in the zone. ✅ positivemedium | positive | medium | |
| DD_003 | Drag card-2 to zone-b (scoped zone locator) Expected: Card lands in zone-b; other zones remain empty. ✅ positivehigh | positive | high | |
| DD_004 | Drop three cards each into their matching zone Expected: All three zones contain their correct cards; result confirms. ✅ positivehigh | positive | high | |
| DD_005 | Drag wrong card to zone — verify zone rejects or swaps Expected: Zone either shows an error state or reflects the swapped card. ⚠️ edgelow | edge | low | |
| DD_006 | Reorder sortable list: move item-3 above item-1 Expected: List order updates; item-3 is now the first visible item. ✅ positivehigh | positive | high | |
| DD_007 | Sortable list reflects new order in result text Expected: Result span shows the new comma-separated order after reorder. ✅ positivemedium | positive | medium | |
| DD_008 | Move task-2 from Todo column to Done column (kanban) Expected: task-2 no longer appears in the Todo column; it appears in Done. ✅ positivehigh | positive | high | |
| DD_009 | Locate kanban column heading via ancestor axis (no testid on heading) Expected: Correct column heading found and asserted via XPath ancestor. ✅ positivemedium | positive | medium | |
| DD_010 | Drop matching shape into correct type-restricted zone Expected: Zone accepts the drop; no error message is shown. ✅ positivehigh | positive | high | |
| DD_011 | Drop wrong shape into type-restricted zone — assert rejection Expected: Error message appears; zone does not hold the item. ❌ negativehigh | negative | high | |
| DD_012 | Locate un-testid'd board card via aria-label (challenge board) Expected: Card found by aria-label and dragged to Done column. ✅ positivehigh | positive | high | |
| DD_013 | Move card between all three board columns in sequence Expected: Card moves Backlog → In Progress → Done; result reflects final column. ✅ positivemedium | positive | medium | |
| DD_014 | Reset button restores all scenarios to initial state Expected: After reset, all items return to their start positions. ⚠️ edgelow | edge | low |
Drag-and-drop is one of the harder interactions to automate because it relies on a sequence of pointer events: mousedown, mousemove, mouseup (or the HTML5 drag events: dragstart, dragover, drop). Playwright provides two high-level helpers — page.dragAndDrop() and locator.dragTo() — that handle this sequence reliably. Selenium uses the Actions API with clickAndHold, moveToElement, and release. Cypress does not have a built-in drag helper; most teams use the cypress-drag-drop plugin or trigger the underlying events manually.
page.dragAndDrop(source, target) accepts CSS selectors or text selectors and internally moves the mouse with buttons held down. It works for both native HTML5 drag events and pointer-event-based drag libraries. Always wait for the element to be visible before dragging; if it is hidden or off-screen, the drag will silently fail.
// High-level drag and drop by CSS selector
await page.dragAndDrop(
'[data-testid="dd-item"]',
'[data-testid="dd-drop-zone"]'
);
// With options — source/target position offsets
await page.dragAndDrop(
'[data-testid="dd-item"]',
'[data-testid="dd-drop-zone"]',
{ sourcePosition: { x: 10, y: 10 }, targetPosition: { x: 50, y: 50 } }
);
// Assert the result span updated
await expect(page.locator('#result-s01')).toContainText('dropped');locator.dragTo(targetLocator) is the locator-chained version of dragAndDrop. It is more composable when you already have a locator reference and want to reuse it in assertions. Use the force option sparingly — it bypasses actionability checks and can hide real problems.
// locator.dragTo() — composable with existing locator references
const card = page.locator('[data-testid="dd-card"][data-card-id="card-2"]');
const zone = page.locator('[data-testid="dd-zone"][data-zone-id="zone-b"]');
await card.dragTo(zone);
// Scoped drag: locate card inside a specific column first
const todoCol = page.locator('[data-column-id="todo"]');
const doneCol = page.locator('[data-column-id="done"]');
const task = todoCol.locator('[data-task-id="task-2"]');
await task.dragTo(doneCol);
// Assert task is now in the Done column
await expect(doneCol.locator('[data-task-id="task-2"]')).toBeVisible();When dragAndDrop or dragTo does not work (custom drag libraries that only listen to pointer events), use the low-level mouse API: mouse.move() to hover the source, mouse.down() to press, mouse.move() to the target position, then mouse.up() to release. You can also dispatch a DataTransfer event via page.dispatchEvent() for libraries that read the drag payload.
// Low-level mouse API for custom drag libraries
const source = page.locator('[data-item-type="shape-circle"]');
const target = page.locator('[data-accepts="shape-circle"]');
// Get bounding boxes
const srcBox = await source.boundingBox();
const tgtBox = await target.boundingBox();
if (!srcBox || !tgtBox) throw new Error('Element not found');
// Simulate a drag via mouse events
await page.mouse.move(srcBox.x + srcBox.width / 2, srcBox.y + srcBox.height / 2);
await page.mouse.down();
await page.mouse.move(tgtBox.x + tgtBox.width / 2, tgtBox.y + tgtBox.height / 2, { steps: 10 });
await page.mouse.up();
// For HTML5 drag events — dispatchEvent with DataTransfer
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await source.dispatchEvent('dragstart', { dataTransfer });
await target.dispatchEvent('dragover', { dataTransfer });
await target.dispatchEvent('drop', { dataTransfer });Selenium's Actions class chains pointer actions: clickAndHold(source), moveToElement(target), release(). On some browsers or drag libraries the high-level dragAndDrop shortcut skips intermediate mousemove events and fails. In those cases, build the action chain manually with small intermediate move steps or use a JavaScript executor fallback.
// Playwright equivalent of Selenium Actions manual chain
// (for when dragAndDrop is not reliable with a specific library)
const src = page.locator('[data-testid="dd-sort-item"][data-item-id="item-3"]');
const tgt = page.locator('[data-testid="dd-sort-item"][data-item-id="item-1"]');
const srcBox = await src.boundingBox();
const tgtBox = await tgt.boundingBox();
if (!srcBox || !tgtBox) return;
await page.mouse.move(srcBox.x + srcBox.width / 2, srcBox.y + srcBox.height / 2);
await page.mouse.down();
// Small nudge to start the drag
await page.mouse.move(srcBox.x + srcBox.width / 2 + 1, srcBox.y + srcBox.height / 2);
await page.mouse.move(tgtBox.x + tgtBox.width / 2, tgtBox.y + tgtBox.height / 2, { steps: 15 });
await page.mouse.up();| Action | Selenium | Playwright JS | Playwright PY | Cypress |
|---|---|---|---|---|
| High-level drag and drop | Actions.dragAndDrop(src, tgt) | page.dragAndDrop(src, tgt) | page.drag_and_drop(src, tgt) | .drag(target) (plugin) |
| Locator-chained drag | Actions.dragAndDrop(srcEl, tgtEl) | locator.dragTo(targetLocator) | locator.drag_to(target_locator) | .drag(selector) (plugin) |
| Manual mouse press/move | clickAndHold → moveToElement → release | mouse.down() → mouse.move() → mouse.up() | mouse.down() → mouse.move() → mouse.up() | trigger('dragstart') → trigger('drop') |
| HTML5 event dispatch | JS: dispatchEvent(new DragEvent(…)) | dispatchEvent('dragstart', { dataTransfer }) | dispatch_event('dragstart', {'dataTransfer': dt}) | trigger('dragstart', { dataTransfer }) |
| Bounding box position | element.getRect() → offsetX/offsetY | locator.boundingBox() | locator.bounding_box() | .invoke('offset') / position: 'top' |