Actions: click, fill, hover, type, drag
The verbs of browser automation. Each action has subtle options that change behaviour, knowing them is the difference between flaky and rock-solid scrapers.
What you’ll learn
- Use the seven core action methods: click, fill, type, press, hover, focus, drag.
- Distinguish `fill` (atomic set) from `type` (keystroke simulation).
- Force, dispatch, or modify-key on clicks when actionability checks fail.
- Identify the three scenarios where you should bypass auto-wait and dispatch events directly.
Locators tell Playwright what to find. Actions tell it what to do. The action methods are deceptively simple, every one has options that matter for real scrapes. This lesson is the verbs you'll use every day, with the foot-guns that trip people up.
click
The most common action. Auto-waits for the element to be attached, visible, stable, receiving events, and enabled, then clicks the centre.
page.locator("button.add-to-cart").click()
Options that matter:
| Option | What it does |
|---|---|
force=True |
Skip actionability checks (dangerous; use only when justified). |
button="right" |
Right-click. Also "middle". |
click_count=2 |
Double-click without the cleaner dblclick(). |
modifiers=["Control"] |
Hold modifier keys while clicking. |
position={"x": 10, "y": 5} |
Click at a specific point inside the element. |
delay=100 |
Pause between mousedown and mouseup (ms). |
no_wait_after=True |
Don't wait for navigation to complete after the click. |
Use force=True only when you've verified the element is actually clickable but Playwright's checks disagree (sticky headers, custom-cursor overlays). Don't reach for it by reflex.
dblclick and right-click
page.locator(".row").dblclick()
page.locator(".item").click(button="right")
dblclick() is preferred over click(click_count=2) because some pages bind to the dblclick event distinctly from two clicks.
fill
Sets the value of an <input> or <textarea> atomically. Clears the field first, then writes the new value.
page.locator("#search").fill("yellow mug")
fill does NOT simulate keystrokes. It uses the same path the framework uses internally to update bound state. Works for React's controlled inputs because fill dispatches the right input event after setting the value.
type, keystroke simulation
type simulates pressing each character key in turn:
page.locator("#search").type("yellow mug", delay=50)
Use it when:
- The site has a JS-side autocomplete that fires per keystroke and you need each request to happen.
- An input listens for
keydown/keypress(not justinput),fillwon't trigger those. - You're stress-testing input debouncing.
delay=50 adds 50ms between each character, useful for letting JS handlers settle and for looking less robotic.
For 99% of plain form filling, prefer fill. It's faster and more reliable.
press
Sends a single key event. The key name uses Playwright's key reference, Enter, Tab, Escape, ArrowDown, etc.
page.locator("#search").press("Enter")
page.locator("body").press("Escape")
page.locator("body").press("Control+K") # key chords with +
press is how you submit a form whose submit button isn't easy to find, or how you close a modal that listens for Escape.
hover
Moves the mouse pointer onto the element. Triggers :hover CSS and any mouseenter JS:
page.locator(".tooltip-target").hover()
page.locator(".tooltip").wait_for(state="visible")
print(page.locator(".tooltip").inner_text())
Often required for menus that reveal content on hover. Useful in tandem with wait_for(state="visible") for tooltip content.
focus and blur
page.locator("#search").focus()
# do stuff while focused
page.locator("body").click() # blur by clicking elsewhere
Tab-driven UIs sometimes only reveal content when an input is focused. focus() synthesizes that without keyboard simulation.
drag_to
Drag from one locator to another:
source = page.locator("#item-1")
target = page.locator("#drop-zone")
source.drag_to(target)
Works for HTML5 drag-and-drop on most pages. For canvas-based or custom-implemented drag, you'll need to dispatch mousedown, mousemove, mouseup manually:
box = source.bounding_box()
target_box = target.bounding_box()
page.mouse.move(box["x"] + box["width"]/2, box["y"] + box["height"]/2)
page.mouse.down()
page.mouse.move(target_box["x"] + 5, target_box["y"] + 5, steps=10)
page.mouse.up()
steps=10 interpolates intermediate positions, which some apps need to register the drag.
select_option
For <select> elements:
page.locator("#country").select_option("US") # by value
page.locator("#country").select_option(label="United States") # by visible label
page.locator("#country").select_option(index=2) # by index
page.locator("#tags").select_option(["js", "css"]) # multi-select
Returns the list of actually-selected option values, so you can verify the change took effect.
check / uncheck / set_checked
Boolean inputs:
page.locator("input[name='terms']").check()
page.locator("input[name='subscribe']").uncheck()
page.locator("input[name='subscribe']").set_checked(True)
These are smarter than a raw click: they verify the input's state after the action and retry once if it didn't change.
When actionability checks fail
You'll occasionally see:
Error: locator.click: Timeout 30000ms exceeded.
- element is not visible
- waiting for locator(...) to be visible
Three legitimate fixes, in order of preference:
- Scroll the element into view.
locator.scroll_into_view_if_needed()then re-action. - Wait for an interfering modal to close. Often a cookie banner is on top, dismiss it first (Lesson 2.20).
force=Trueas a last resort. When you've verified the element is genuinely interactable but Playwright's heuristics disagree.
Never default to force=True. It silences the warning without fixing the problem, and you'll get clicks that "succeed" while doing nothing.
dispatch_event, the nuclear option
page.locator("button").dispatch_event("click")
dispatch_event synthesises a DOM event directly, bypassing the entire actionability pipeline. The click happens even if the element is invisible, off-screen, or covered.
Use it when you genuinely want the JS handler to run regardless of UI state, for example, an export button that exists in the DOM but is hidden behind feature flags. Not appropriate for "I can't figure out why the click isn't working" debugging; that path leads to silent scraper failures.
Hands-on lab
Open /challenges/dynamic/click-required/reveal. The page hides content behind a "Reveal" button. Write a script that: (1) clicks Reveal, (2) waits for the revealed content to appear, (3) extracts its text. Then try the same scrape with dispatch_event('click') and compare, both should work for this page, but understand why one is preferred.
Hands-on lab
Practice this lesson on Catalog108, our first-party scraping sandbox.
Open lab target →/challenges/dynamic/click-required/revealQuiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.