ScreenshotNeo

BlogHow-to

How to Drag an Element With the Puppeteer Mouse API

Drag an element in Puppeteer with page.mouse.move(), down(), and up(). Learn coordinate handling, dragAndDrop(), common failure cases, and a complete runnable example.

By the ScreenshotNeo team4 October 20267 min read

To drag an element with Puppeteer’s low-level mouse API, move the mouse to a point on the source, press the button, move to the destination while holding it, then release:

await page.mouse.move(sourceX, sourceY);
await page.mouse.down();
await page.mouse.move(targetX, targetY, { steps: 10 });
await page.mouse.up();

Puppeteer mouse coordinates are main-frame CSS pixels measured from the viewport’s top-left corner. The code above models a pointer movement; it does not guarantee that every site’s drag-and-drop implementation will accept it. For HTML drag-and-drop, Puppeteer also provides page.mouse.dragAndDrop(start, target), which performs drag, dragenter, dragover, and drop. See the official Mouse API, dragAndDrop() reference, and page interactions guide.

1. Run a complete mouse drag

This Node.js example launches Chromium, opens a page, measures two elements, drags from the center of the source to the center of the target, and closes the browser. Replace the example URL and selectors with your page’s URL and draggable element selectors.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const sourceBox = await page.locator('[data-testid="source"]').boundingBox();
  const targetBox = await page.locator('[data-testid="target"]').boundingBox();

  if (!sourceBox || !targetBox) {
    throw new Error('Drag source or target is not in layout');
  }

  const source = {
    x: sourceBox.x + sourceBox.width / 2,
    y: sourceBox.y + sourceBox.height / 2,
  };
  const target = {
    x: targetBox.x + targetBox.width / 2,
    y: targetBox.y + targetBox.height / 2,
  };

  await page.mouse.move(source.x, source.y);
  await page.mouse.down();
  await page.mouse.move(target.x, target.y, { steps: 10 });
  await page.mouse.up();

  // Verify the application's resulting state here.
} finally {
  await browser.close();
}

Install Puppeteer in your project with npm install puppeteer. The selector values are examples: the page must actually contain matching elements, and the source point must land on the part of the UI that begins dragging. If a widget has a drag handle, measure the handle and use its box instead of the whole item.

2. Choose the right drag method

Method Use it for What to know
move() + down() + move() + up() Coordinate control, custom paths, or UI that responds to mouse movement while pressed You control the press and release sequence. A move with intermediate steps can provide a path.
page.mouse.dragAndDrop(start, target, options) A drag requiring the drag, dragenter, dragover, and drop event sequence Coordinates are points. Optional delay is the wait between dragover and drop, in milliseconds; its documented default is zero.
page.mouse.drag(start, target) When you specifically need the drag event API and returned drag data The reference describes dispatching a drag event. It does not describe the complete dragenter/dragover/drop sequence.
Locator actions Finding elements and waiting for readiness Locators wait for elements and check readiness conditions for supported actions. For a custom coordinate drag, get a bounding box after the relevant element is ready.

The official API reference marks ElementHandle.dragAndDrop() obsolete and points to ElementHandle.drop(). Check the documentation for your installed Puppeteer version before adopting an ElementHandle helper: API references may differ by version. The page.mouse.dragAndDrop() API is a separate Mouse method.

3. Get stable coordinates from elements

Use locators to select elements and wait for the page to be ready, then obtain their boxes for coordinate-based input. Puppeteer recommends locators for selection and interaction because they wait for an element and check relevant preconditions. A locator’s readiness behavior is not a guarantee that a later custom mouse gesture will work: the page can still change between measuring the box and moving the pointer.

const handle = await page.locator('[data-testid="drag-handle"]').waitHandle();
const box = await handle.boundingBox();

if (!box) {
  throw new Error('Drag handle is not in layout');
}

const start = {
  x: box.x + box.width / 2,
  y: box.y + box.height / 2,
};

boundingBox() returns a box relative to the main frame, or null if the element is not part of layout. Keep the coordinate systems aligned: page.mouse uses main-frame viewport CSS pixels. Do not multiply the measured coordinates by a device scale factor or add page scroll offsets when using these viewport-relative boxes.

The center is a convenient starting point, not a Puppeteer requirement. For a small handle, a nested control, or a custom canvas, choose a point inside the actual drag region. If the element moves during the interaction, re-evaluate the geometry or make the page state stable before beginning.

4. Control the movement

page.mouse.move(x, y, options) accepts a destination point. Its optional steps setting controls the number of intermediate movements between the current point and destination; the documented default is 1. For example:

await page.mouse.move(target.x, target.y, { steps: 12 });

More steps can help when an application reacts to the path rather than only the endpoints, but no particular step count is guaranteed to fix a site’s drag behavior. The drag-and-drop helper has a different option: delay waits between dragover and drop. Don’t confuse that delay with movement steps.

5. Verify the drop and handle failures

Finishing the input calls only proves that Puppeteer issued them. Check the application’s outcome, such as the item’s new parent, order, position, or a success state.

Symptom Likely cause What to try
Source or target box is null The element is not part of layout, has been removed, or the selector did not resolve as expected. Wait for the correct element, confirm the selector, and ensure it is rendered before measuring.
Drag starts from the wrong place The element’s center is not its drag handle, or an overlay covers that point. Measure the handle specifically, choose an interior point that initiates dragging, and check for overlays.
Pointer reaches the target but nothing changes The UI may require HTML drag-and-drop events, a different input model, or an application-specific gesture. Try page.mouse.dragAndDrop() for a dragenter/dragover/drop sequence; inspect the page’s expected interaction and verify its final state.
Drag works inconsistently The page layout may shift after coordinates are measured, or the target may move during the gesture. Wait for a stable page state, measure just before dragging, and add movement steps if the UI depends on the path.
An error occurs after pressing the mouse The sequence may have failed before release, leaving the interaction incomplete. Structure your code so the mouse is released in cleanup when possible; reset or recreate the page state before retrying.
Dragging text selection does not behave like a user gesture Puppeteer documents that its mouse events are synthetic and do not fully reproduce normal mouse functionality; its Mouse reference specifically says dragging and selecting text is not possible using page.mouse. Use an interaction method suitable for the feature under test, or test the page’s application-level state instead of assuming native text dragging will work.

For diagnosis, log the measured source and target points, capture a screenshot before and after the interaction, and inspect the DOM state that should change. These checks help distinguish a coordinate problem from an event-model mismatch.

6. Reliability, performance, and cost

Keep the browser page alive between related actions when practical; launching a new browser for each drag adds setup work. Prefer stable selectors and measure coordinates close to the gesture so responsive layout changes or late-loading content are less likely to invalidate them. Avoid arbitrary waits where a concrete readiness condition or observable result is available.

Movement steps add intermediate input events, so use only enough to match the behavior under test. A short, direct path is usually easier to diagnose than a long sequence. Browser automation cost depends on your runtime, browser hosting, and how often jobs execute; Puppeteer itself does not establish a universal price or performance figure. For reliability, close the browser in a finally block and verify the result before treating the drag as successful.

Or skip the browser setup

If your goal is to capture the page after arranging or inspecting it, ScreenshotNeo is a website screenshot API and MCP server. For interactive drag testing, use Puppeteer; for a screenshot, one API call can return an image or PDF without setting up a browser capture flow.

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

Equivalent requests:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
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 import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the ScreenshotNeo API documentation for options and setup, then sign up for 1,000 free screenshots a month, with no card.

FAQ

Does page.mouse.move() hold the mouse button?

No. Call page.mouse.down() before moving to the destination and page.mouse.up() to release.

Are drag coordinates measured in screen pixels?

No. They are CSS pixels relative to the main-frame viewport’s top-left corner.

Should I use the center of the draggable element?

It is a practical default when that point is inside the active drag region. Use the widget’s handle or another suitable point when its behavior requires it.

Does a successful mouse.up() mean the drop succeeded?

No. Confirm the resulting page or application state.