Action Chains — Mouse Hover, Drag-and-Drop and Keyboard Combos

Simple clicks and keystrokes cover most automation needs, but real web applications demand more: hovering over menus to reveal dropdowns, dragging items between lists, holding Shift while clicking to multi-select, right-clicking for context menus, and double-clicking to edit inline content. Selenium’s ActionChains API handles all of these by letting you compose sequences of low-level mouse and keyboard actions into a single performable chain.

ActionChains — Composing Complex Interactions

ActionChains works by queuing actions and executing them in order when you call .perform(). Each action builds on the previous one, allowing you to create multi-step interactions that mirror real user behaviour.

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

# driver = webdriver.Chrome()

# ── 1. Mouse Hover (reveal dropdown menu) ──
def hover_and_click_menu_item(driver):
    menu = driver.find_element(By.CSS_SELECTOR, ".nav-menu")
    submenu_item = driver.find_element(By.CSS_SELECTOR, ".submenu-item")

    actions = ActionChains(driver)
    actions.move_to_element(menu)       # Hover over the menu
    actions.pause(0.5)                   # Wait for dropdown animation
    actions.click(submenu_item)          # Click the revealed item
    actions.perform()                    # Execute the chain


# ── 2. Drag and Drop ──
def drag_item_to_target(driver):
    source = driver.find_element(By.ID, "draggable-item")
    target = driver.find_element(By.ID, "drop-zone")

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

    # Alternative: drag by offset (pixels)
    # actions.drag_and_drop_by_offset(source, x_offset=200, y_offset=0)


# ── 3. Right-click (context menu) ──
def right_click_element(driver):
    element = driver.find_element(By.ID, "context-target")

    actions = ActionChains(driver)
    actions.context_click(element)       # Right-click
    actions.perform()

    # Now interact with the context menu that appeared
    menu_option = WebDriverWait(driver, 5).until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, ".context-menu .delete"))
    )
    menu_option.click()


# ── 4. Double-click (inline edit) ──
def double_click_to_edit(driver):
    cell = driver.find_element(By.CSS_SELECTOR, "td.editable")

    actions = ActionChains(driver)
    actions.double_click(cell)
    actions.perform()

    # Cell should now show an input field for editing
    input_field = cell.find_element(By.TAG_NAME, "input")
    input_field.clear()
    input_field.send_keys("Updated Value")
    input_field.send_keys(Keys.ENTER)


# ── 5. Keyboard combos (Ctrl+A, Ctrl+C, Ctrl+V) ──
def select_all_and_copy(driver):
    text_area = driver.find_element(By.ID, "source-text")

    actions = ActionChains(driver)
    actions.click(text_area)
    actions.key_down(Keys.CONTROL)       # Hold Ctrl
    actions.send_keys("a")               # Ctrl+A (select all)
    actions.send_keys("c")               # Ctrl+C (copy)
    actions.key_up(Keys.CONTROL)         # Release Ctrl
    actions.perform()


# ── 6. Shift+Click for multi-select ──
def shift_click_multi_select(driver):
    items = driver.find_elements(By.CSS_SELECTOR, ".list-item")

    actions = ActionChains(driver)
    actions.click(items[0])              # Click first item
    actions.key_down(Keys.SHIFT)         # Hold Shift
    actions.click(items[4])              # Shift+Click fifth item (selects 1-5)
    actions.key_up(Keys.SHIFT)           # Release Shift
    actions.perform()


# ── Summary of ActionChains methods ──
METHODS = [
    ("move_to_element(el)",         "Hover over an element"),
    ("click(el)",                   "Left-click an element"),
    ("double_click(el)",            "Double-click an element"),
    ("context_click(el)",           "Right-click an element"),
    ("drag_and_drop(src, tgt)",     "Drag source to target element"),
    ("drag_and_drop_by_offset(el, x, y)", "Drag by pixel offset"),
    ("key_down(key)",               "Press and hold a key (Shift, Ctrl, Alt)"),
    ("key_up(key)",                 "Release a held key"),
    ("send_keys(text)",             "Type text at the current focus"),
    ("send_keys_to_element(el, t)", "Type text into a specific element"),
    ("pause(seconds)",              "Wait between actions in the chain"),
    ("perform()",                   "Execute the queued action chain"),
    ("reset_actions()",             "Clear the action queue"),
]

print("ActionChains Method Reference")
print("=" * 60)
for method, desc in METHODS:
    print(f"  .{method:<38} {desc}")
Note: ActionChains uses the W3C Actions API, which models three input devices: a pointer (mouse), a keyboard, and a wheel (scroll). Each .perform() call sends the entire queued sequence as a single atomic operation to the browser driver. This is why you must call .perform() at the end — without it, no actions are executed. If you need to reset the queue after performing, call .reset_actions() to clear it before building a new chain.
Tip: When drag-and-drop does not work with drag_and_drop(source, target) — which happens with some JavaScript frameworks like React DnD or SortableJS — try the lower-level approach: actions.click_and_hold(source).pause(0.5).move_to_element(target).pause(0.5).release().perform(). Breaking the drag into explicit steps with pauses gives the JavaScript event handlers time to process each phase (mousedown, mousemove, mouseup) and often fixes the issue.
Warning: On macOS, keyboard shortcuts use Command (⌘) instead of Control. If your tests use Keys.CONTROL for shortcuts like Ctrl+A, they will fail on macOS because the correct modifier is Keys.COMMAND. For cross-platform tests, detect the OS and use the appropriate modifier: modifier = Keys.COMMAND if platform == "darwin" else Keys.CONTROL. Alternatively, use the Keys.META alias which maps to the platform's primary modifier key.

Common Mistakes

Mistake 1 — Forgetting to call .perform() at the end of the chain

❌ Wrong: ActionChains(driver).move_to_element(menu).click(submenu) — the actions are queued but never executed.

✅ Correct: ActionChains(driver).move_to_element(menu).click(submenu).perform() — the .perform() call sends the queued actions to the browser.

Mistake 2 — Not adding pauses in hover-to-reveal interactions

❌ Wrong: actions.move_to_element(menu).click(submenu).perform() — the click fires before the dropdown animation finishes, clicking on empty space.

✅ Correct: actions.move_to_element(menu).pause(0.5).click(submenu).perform() — the pause gives the CSS animation time to reveal the dropdown before the click.

🧠 Test Yourself

You need to hover over a navigation menu to reveal a dropdown, then click a submenu item. Which ActionChains sequence is correct?