Scraping Central is reader-supported. When you buy through links on our site, we may earn an affiliate commission.

2.8intermediate5 min read

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 just input), fill won'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:

  1. Scroll the element into view. locator.scroll_into_view_if_needed() then re-action.
  2. Wait for an interfering modal to close. Often a cookie banner is on top, dismiss it first (Lesson 2.20).
  3. force=True as 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/reveal

Quiz, check your understanding

Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.

Actions: click, fill, hover, type, drag1 / 8

What's the difference between `locator.fill('text')` and `locator.type('text')`?

Score so far: 0 / 0