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}")
.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.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.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.