ScreenshotNeo

BlogHow-to

How to Drag and Drop Elements With Puppeteer

Use Puppeteer locators to find elements, then drag between their viewport coordinates with page.mouse.dragAndDrop().

By the ScreenshotNeo team4 October 20266 min read

Use Puppeteer locators to find the source and destination, read their bounding boxes, then call page.mouse.dragAndDrop(start, target, options) with viewport coordinates. The method dispatches a drag sequence; verify the page’s resulting state to confirm the application accepted the drop.

Runnable example

This example uses Puppeteer’s current coordinate-based mouse API. It navigates to a page containing #source and #target, drags from the center of each element, and checks an application-visible result. Replace the selectors and assertion with those for your page.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/board', { waitUntil: 'domcontentloaded' });

  const sourceLocator = page.locator('#source');
  const targetLocator = page.locator('#target');
  await sourceLocator.wait();
  await targetLocator.wait();

  const source = await sourceLocator.boundingBox();
  const target = await targetLocator.boundingBox();
  if (!source || !target) {
    throw new Error('Source or target has no visible layout box');
  }

  await page.mouse.dragAndDrop(
    { x: source.x + source.width / 2, y: source.y + source.height / 2 },
    { x: target.x + target.width / 2, y: target.y + target.height / 2 },
    { delay: 100 },
  );

  // Assert the outcome your application exposes, such as a changed parent,
  // updated list order, or a success message.
  await page.locator('#target #source').wait();
} finally {
  await browser.close();
}

Install Puppeteer with npm install puppeteer and save the example as an ES module, such as drag.mjs. Run it with node drag.mjs. The sample assumes the item becomes a child of the target; many applications instead update an order, class, or status.

For a stable test, point the browser to a page you control or a test fixture. A public page may change its markup or drag behavior without notice.

How the drag works

page.mouse.dragAndDrop(start, target, options?) accepts two points, not selectors or element handles. Puppeteer documents the sequence as drag, dragenter, dragover, and drop. The optional delay is a wait in milliseconds between dragover and drop; its default is zero. See the Mouse.dragAndDrop API.

Mouse coordinates are CSS pixels relative to the top-left of the main-frame viewport. They are not document coordinates. Puppeteer’s mouse events are synthetic, so they do not reproduce every interaction a physical mouse can perform. See the Mouse class reference.

Choosing points and selectors

Puppeteer recommends locators for selecting and interacting with elements. Locators wait for elements to be present and ready for an action; use them to select and measure, then use the lower-level mouse API for the drag gesture. See the page interactions guide.

  • Start point: The center is a useful default for a simple draggable element. If the page provides a drag handle, calculate a point inside that handle instead.
  • Drop point: Choose a point within the actual drop zone. The center may be wrong when the page accepts drops only on a particular edge or placeholder.
  • Visibility: A locator match does not guarantee a usable box at the moment you measure. Check for a null bounding box and wait for the page state that reveals the element.
  • Frames: Mouse coordinates are defined relative to the main frame viewport. For elements inside an iframe, use the appropriate frame to locate and measure them, and confirm that the resulting coordinates correspond to the main page’s mouse coordinate space before dragging.
  • Scrolling and layout changes: Measure close to the drag, after the page has settled. If scrolling, animation, or responsive layout changes move the elements, stale coordinates can land elsewhere.

Adjust the gesture for your page

Use a handle or a different target point

Compute coordinates from the handle’s bounding box rather than the whole draggable item when the page requires a handle. Likewise, target the intended insertion marker or drop-zone region. The API takes points, so point selection is where page-specific behavior enters.

Add a delay when the page needs it

await page.mouse.dragAndDrop(start, target, { delay: 250 });

The delay is specifically between dragover and drop. It can help a delay-sensitive interface process the drag-over state. It does not change the documented event sequence or guarantee that an application recognizes the gesture.

Confirm an application outcome

Do not treat a completed method call as proof that the drop succeeded. Wait for an observable result: an item moved to a container, a list order changed, a drop indicator appeared, or the application saved a new state. The best assertion depends on the page’s behavior.

Common errors and fixes

Symptom Likely cause What to do
Bounding box is null The element has no visible layout box yet, or the locator matched an element that is hidden. Wait for the relevant page state, check the selector, and ensure the intended element is visible before measuring.
The drag runs but nothing moves The start point missed the draggable handle, the target point missed the drop zone, or the app expects a different gesture. Inspect the page’s draggable area and drop-zone boundaries. Try points inside those regions and assert the resulting page state.
The result is intermittent Coordinates were measured before layout settled, or the page is sensitive to event timing. Wait for the relevant content, measure immediately before dragging, and try a short delay between drag-over and drop.
An old example calls ElementHandle.dragAndDrop() That API is marked obsolete in Puppeteer’s reference. Use the documented page.mouse.dragAndDrop() coordinate API for this pattern. The obsolete reference points to ElementHandle.drop; see its API page.
A custom drag interaction still fails Puppeteer’s synthetic mouse events may not reproduce the interaction the application expects. Inspect the application’s drag implementation and determine which event behavior it relies on. Keep an assertion for the actual outcome.

Reliability, performance, and cost

Bounding-box lookups and one drag gesture are small operations; the main sources of slow or flaky automation are usually page loading, changing layout, and waiting for an application-specific result. Prefer a targeted readiness condition and a state assertion over arbitrary long sleeps. A nonzero drag delay adds that wait between drag-over and drop.

For reliability, use stable selectors, measure after relevant content is visible, select points inside the real handle and drop zone, and verify the resulting state. Synthetic events have documented limits, so a passing script should be understood as validating the page behavior exercised by this Puppeteer interaction, not every possible physical-mouse behavior.

Puppeteer itself is an open-source browser automation library; this interaction has no per-drag API charge described in the cited Puppeteer documentation. Running a browser still consumes local or hosted compute resources. If you use a managed browser, its provider’s pricing and limits are separate; Browserless documents connecting Puppeteer to a managed browser through its service, but that infrastructure is optional: Browserless BaaS documentation.

Or skip the browser setup

If your goal is a screenshot of the page before or after an interaction, ScreenshotNeo can return an image or PDF with one API request. It does not perform Puppeteer drag-and-drop actions. For screenshots, cookie banners, popups, and chat widgets are removed before capture; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. The ScreenshotNeo docs describe the API.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Sign up for 1,000 free screenshots a month with no card.

FAQ

Can I pass selectors directly to dragAndDrop?

No. The documented mouse method takes start and target points. Use locators to find elements and derive coordinates from their bounding boxes.

What does the delay option wait for?

It waits between the dragover and drop events, in milliseconds. It defaults to zero.

Does this guarantee native mouse behavior?

No. Puppeteer documents that its mouse events are synthetic and do not fully reproduce everything a normal mouse can do.

Should I use ElementHandle.dragAndDrop()?

The method is marked obsolete. Prefer the current documented mouse API for coordinate-based drag-and-drop.