ScreenshotNeo

BlogHow-to

How to Drag and Drop Elements with Selenium and Python

Use Selenium Python's ActionChains to drag an element to a target or by an offset, then verify the page accepted the change.

By the ScreenshotNeo team4 October 20267 min read

Use Selenium Python’s ActionChains to drag one element onto another: locate the source and destination WebElements, call drag_and_drop(source, target).perform(), then assert that the page reflects the new state. If the destination is defined by a displacement instead of a target element, use drag_and_drop_by_offset(source, xoffset, yoffset).perform().

Selenium’s current Python API describes drag_and_drop as holding the left mouse button on the source, moving to the target, and releasing. That describes the browser input gesture; a successful gesture does not guarantee that every site’s custom drag handler accepted it.

1. Install Selenium and prepare a driver

Install the Python binding in the same environment that runs your test:

python -m pip install selenium

The runnable example below uses Selenium Manager, which can manage a compatible browser driver when a supported browser is installed. It opens a page, performs a drag, and waits for an application-level result. Replace the example URL and selectors with those from your own page.

2. Complete runnable Python example

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

URL = "https://example.com/board"
SOURCE = (By.CSS_SELECTOR, "[data-testid='card-1']")
TARGET = (By.CSS_SELECTOR, "[data-testid='column-done']")

options = webdriver.ChromeOptions()
# Uncomment for headless execution in CI:
# options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
try:
    driver.get(URL)
    wait = WebDriverWait(driver, 10)
    source = wait.until(EC.visibility_of_element_located(SOURCE))
    target = wait.until(EC.visibility_of_element_located(TARGET))

    ActionChains(driver).drag_and_drop(source, target).perform()

    # Prefer a result assertion over assuming the input gesture succeeded.
    wait.until(lambda d: "done" in d.find_element(*SOURCE).get_attribute("data-column"))
    print("Card moved to the done column")
finally:
    driver.quit()

This example expects the application to update the source element’s data-column attribute. Change the wait condition to match a meaningful result in your app, such as a changed parent, new list order, or confirmation message. The example selectors and URL are placeholders, not a claim about a live page.

3. Choose the right drag method

Method Use it when Arguments
drag_and_drop(source, target) You can locate a destination element. Two WebElements.
drag_and_drop_by_offset(source, xoffset, yoffset) The endpoint is a displacement from the source element. WebElement and integer X/Y offsets in pixels.

For a target element, the concise form is:

source = driver.find_element(By.ID, "source")
target = driver.find_element(By.ID, "target")
ActionChains(driver).drag_and_drop(source, target).perform()

For an offset, use:

source = driver.find_element(By.ID, "source")
ActionChains(driver).drag_and_drop_by_offset(source, 120, 40).perform()

Offsets are relative to the source element’s pointer position as Selenium moves the pointer. They are not screen coordinates. If the page layout changes, an offset that used to land inside a drop zone may miss it; prefer a destination locator when one is available.

4. ActionChains options and execution

ActionChains queues input actions. The sequence is sent to the browser when you call .perform(); omitting it means the queued drag is not executed. Selenium’s current Python API documents a default pointer movement duration of 250 milliseconds, which can be set when constructing the chain:

from selenium.webdriver.common.action_chains import ActionChains

# Set pointer move duration to 500 milliseconds.
actions = ActionChains(driver, duration=500)
actions.drag_and_drop(source, target).perform()

Use a longer duration only when the page’s behavior depends on pointer movement timing or a slower path. It can make the interaction take longer and does not by itself fix a page-specific drag implementation.

For a custom path, the lower-level sequence can make the steps explicit:

actions = ActionChains(driver)
actions.click_and_hold(source)
actions.move_to_element(target)
actions.release()
actions.perform()

These methods also queue actions. Keep the press, movement, and release in order and perform the chain once. The standard method is simpler when its gesture matches the page.

5. Locators, waits, and result checks

  • Use stable attributes such as test IDs or application-owned IDs when available. Avoid selectors tied to styling or changing layout.
  • Wait for both source and target to be visible and interactable before dragging. Finding an element only proves it exists in the DOM.
  • After the gesture, wait for the application result. Drag-and-drop may update asynchronously, so an immediate assertion can race the page.
  • Assert a state the user cares about: parent container, item order, status attribute, or visible confirmation. Do not treat the lack of a Selenium exception as proof of a successful move.

For example, a parent-container assertion might be:

wait.until(lambda d: d.find_element(*SOURCE).find_element(By.XPATH, "..").get_attribute("data-testid") == "column-done")

Adjust the relationship and attribute to match the actual DOM. Some applications render a replacement node after a move; in that case, locate it again inside the wait instead of reusing a stale WebElement.

6. Troubleshooting

Symptom Likely cause What to check or change
AttributeError or method appears unavailable The import or object is not the Selenium ActionChains class. Import from selenium.webdriver.common.action_chains and check the installed Selenium package.
perform() returns without a visible move The gesture ran, but the page did not accept it, or the drop landed outside the active region. Check source and target locators, visibility, overlays, and the app’s resulting DOM state. Try locating the actual drop zone rather than guessing an offset.
StaleElementReferenceException The page replaced or re-rendered an element after lookup. Re-find source and target immediately before the drag, and re-locate elements inside post-drag waits.
ElementNotInteractableException or click interception An element is hidden, covered, outside the usable viewport, or not ready. Wait for visibility, inspect overlays and scrolling, and identify the visible draggable handle or drop surface.
Offset drag is inconsistent Responsive layout, scroll position, or element size changes the landing point. Prefer a target WebElement; if offsets are necessary, calculate them from the current layout and verify the landing result.
Works locally but not in CI Different viewport, browser setup, timing, or headless rendering changes the page geometry or readiness. Use a deliberate window size, wait for the app’s ready state, and assert the result. Do not rely on a fixed sleep as the only readiness check.

When the basic action does not trigger the expected result, inspect how the page implements dragging and which region accepts a drop. Selenium’s general documentation does not establish one universal workaround or a browser-by-browser compatibility guarantee for custom drag handlers. Avoid assuming synthetic JavaScript events will fix the issue; first identify the page-specific behavior.

7. Reliability, performance, and cost

A drag is a short interaction, but reliability depends on the page being in the expected state and on checking the outcome. Prefer explicit waits for readiness and postconditions. Keep a single browser session for related steps, and avoid arbitrary pauses that make a suite slower without proving readiness. The documented 250 ms pointer duration is a default movement setting, not a benchmark or guarantee of total operation time.

For repeatable runs, control the viewport and use stable locators; responsive layouts and animation can alter where a drop lands. If a test fails intermittently, capture the relevant page state and determine whether the source, target, overlay, or application state differed. Selenium itself does not charge per drag; the costs to consider are the browser and compute resources used by your test environment.

8. Inspect the page before automating it

When you need to inspect a page visually to identify its layout or drop zones, ScreenshotNeo is a website screenshot API and MCP server. A screenshot can help you inspect the visible page, but it does not perform or validate a Selenium drag. For interaction tests, continue to use Selenium and assert the page’s resulting state.

Or skip the browser setup

If your task is to capture the page rather than test a drag interaction, ScreenshotNeo returns a screenshot or PDF from one GET request. See the ScreenshotNeo API documentation for options and response details.

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)
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}`);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed. Response headers report the page verdict and billing status.
  • An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, take screenshots, get page information, and capture PDFs.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.

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

9. FAQ

Does Selenium’s drag method prove the item moved?

No. It sends the pointer gesture; assert an application-specific postcondition to confirm the page accepted it.

Should I use an offset or a target element?

Use a target element when there is a meaningful destination to locate. Use an offset when the endpoint is naturally expressed as a displacement.

Can I use this for every custom drag-and-drop widget?

The standard API covers the mouse gesture, but custom widgets can handle it differently. Inspect the specific page behavior and verify the result instead of assuming a universal workaround.