Skip to content

Mouse Control

The Mouse API provides complete control over mouse input at the page level, enabling you to simulate realistic cursor movement, clicks, double-clicks, and drag operations. By default, all mouse operations use humanized simulation: paths follow natural Bezier curves with Fitts's Law timing, minimum-jerk velocity profiles, physiological tremor, and overshoot correction, making automation virtually indistinguishable from human behavior.

Centralized Mouse Interface

All mouse operations are accessible via tab.mouse, providing a clean, unified API for all mouse interactions.

Quick Start

from pydoll.browser.chromium import Chrome
from pydoll.protocol.input.types import MouseButton

async with Chrome() as browser:
    tab = await browser.start()
    await tab.go_to('https://example.com')

    # Move cursor to position (humanized by default)
    await tab.mouse.move(500, 300)

    # Click at position (humanized by default)
    await tab.mouse.click(500, 300)

    # Right-click
    await tab.mouse.click(500, 300, button=MouseButton.RIGHT)

    # Double-click
    await tab.mouse.double_click(500, 300)

    # Drag from one position to another
    await tab.mouse.drag(100, 200, 500, 400)

Core Methods

move: Move Cursor

Move the mouse cursor to a specific position on the page:

# Humanized move (default, curved path with natural timing)
await tab.mouse.move(500, 300)

# Instant move (single CDP event, no simulation)
await tab.mouse.move(500, 300, humanize=False)

Parameters:

  • x: Target X coordinate (CSS pixels)
  • y: Target Y coordinate (CSS pixels)
  • humanize (keyword-only): Simulate human-like curved movement (default: True)

click: Click at Position

Move to position and perform a mouse click:

from pydoll.protocol.input.types import MouseButton

# Left click with humanized movement (default)
await tab.mouse.click(500, 300)

# Right click
await tab.mouse.click(500, 300, button=MouseButton.RIGHT)

# Double click via click_count
await tab.mouse.click(500, 300, click_count=2)

# Instant click without humanization
await tab.mouse.click(500, 300, humanize=False)

Parameters:

  • x: Target X coordinate
  • y: Target Y coordinate
  • button (keyword-only): Mouse button, one of LEFT, RIGHT, MIDDLE (default: LEFT)
  • click_count (keyword-only): Number of clicks (default: 1)
  • humanize (keyword-only): Simulate human-like behavior (default: True)

double_click: Double-Click at Position

Convenience method equivalent to click(x, y, click_count=2):

await tab.mouse.double_click(500, 300)
await tab.mouse.double_click(500, 300, humanize=False)

down / up: Low-Level Button Control

Press or release mouse buttons independently:

# Press left button at current position
await tab.mouse.down()

# Release left button
await tab.mouse.up()

# Right button
await tab.mouse.down(button=MouseButton.RIGHT)
await tab.mouse.up(button=MouseButton.RIGHT)

These are primitives that operate at the current cursor position and have no humanize parameter.

drag: Drag and Drop

Move from start to end while holding the mouse button:

# Humanized drag (default)
await tab.mouse.drag(100, 200, 500, 400)

# Instant drag without humanization
await tab.mouse.drag(100, 200, 500, 400, humanize=False)

Parameters:

  • start_x, start_y: Start coordinates
  • end_x, end_y: End coordinates
  • humanize (keyword-only): Simulate human-like drag (default: True)

Disabling Humanization

All mouse methods default to humanize=True. To perform instant, non-humanized operations, pass humanize=False:

# Instant move, single CDP mouseMoved event
await tab.mouse.move(500, 300, humanize=False)

# Instant click: move + press + release, no simulation
await tab.mouse.click(500, 300, humanize=False)

# Instant drag, no curves, no pauses
await tab.mouse.drag(100, 200, 500, 400, humanize=False)

This is useful when speed is critical and detection evasion is not a concern, for example in testing environments or internal tools.

Humanized Mode

When humanize=True (the default), the mouse module applies multiple layers of realism:

Bezier Curve Paths

Mouse follows a natural curved trajectory instead of a straight line. Control points are randomly offset perpendicular to the start→end line, with asymmetric placement (more curvature early in the movement, like a real ballistic reach).

Fitts's Law Timing

Movement duration follows Fitts's Law: MT = a + b × log₂(D/W + 1). Longer distances take proportionally more time, matching human motor control behavior.

Minimum-Jerk Velocity Profile

The cursor follows a bell-shaped speed profile, starting slow, accelerating to peak velocity in the middle, then decelerating at the end. This matches the smoothest possible human movement trajectory.

Physiological Tremor

Small Gaussian noise (σ ≈ 1px) is added to each frame, simulating hand tremor. The tremor amplitude scales inversely with velocity, with more tremor when the cursor is slow or hovering and less during fast ballistic movements.

Overshoot and Correction

For fast, long-distance movements (~70% probability), the cursor overshoots the target by 3–12% of the distance, then makes a small corrective sub-movement back to the target. This matches real human motor control data.

Pre-Click Pause

Humanized clicks include a pre-click pause (50–200ms) that simulates the natural settle time before pressing the button.

Automatic Humanized Element Clicks

When you use element.click(), the Mouse API is automatically used to produce a realistic Bezier curve movement from the current cursor position to the element center before clicking. This means every element.click() call generates natural mouse movement, making element clicks indistinguishable from human behavior.

# Humanized click (default): Bezier curve movement + click
button = await tab.find(id='submit')
await button.click()

# With offset from center
await button.click(x_offset=10, y_offset=5)

# Disable humanization: raw CDP press/release, no cursor movement
await button.click(humanize=False)

Position tracking is maintained across element clicks. Clicking element A, then element B, produces a natural curved path from A's position to B.

Custom Timing Configuration

All humanization parameters are configurable via MouseTimingConfig:

from pydoll.interactions.mouse import MouseTimingConfig

config = MouseTimingConfig(
    fitts_a=0.070,              # Fitts's Law intercept (seconds)
    fitts_b=0.150,              # Fitts's Law slope (seconds/bit)
    frame_interval=0.012,       # Base interval between mouseMoved events
    curvature_min=0.10,         # Min path curvature as fraction of distance
    curvature_max=0.30,         # Max path curvature
    tremor_amplitude=1.0,       # Tremor sigma in pixels
    overshoot_probability=0.70, # Chance of overshoot on fast moves
    min_duration=0.08,          # Minimum movement duration
    max_duration=2.5,           # Maximum movement duration
)

# Apply to the tab's mouse instance
tab.mouse.timing = config

See the MouseTimingConfig dataclass for all available parameters.

Position Tracking

The Mouse API tracks the cursor position across operations:

# Initial position is (0, 0)
await tab.mouse.move(100, 200)
# Position is now (100, 200)

await tab.mouse.click(300, 400)
# Position is now (300, 400)

# Low-level methods use the tracked position
await tab.mouse.down()   # Presses at (300, 400)
await tab.mouse.up()     # Releases at (300, 400)

Position State

The mouse position is tracked internally. WebElement.click() automatically uses tab.mouse when available, so position tracking is maintained across element clicks.

Debug Mode

Enable debug mode to visualize mouse movement on the page. When active, colored dots are drawn on a transparent overlay canvas:

  • Blue dots: cursor path during movement
  • Red dots: click positions
# Enable at runtime via property
tab.mouse.debug = True

# Now all movements draw colored dots
await tab.mouse.click(500, 300)

# Disable when done
tab.mouse.debug = False

This is useful for tuning timing parameters and verifying that paths look natural.

Practical Examples

Click a Button with Realistic Movement

async def click_button_naturally(tab):
    # element.click() automatically uses tab.mouse for humanized movement
    button = await tab.find(id='submit')
    await button.click()

Drag a Slider

async def drag_slider(tab):
    slider = await tab.find(css_selector='.slider-handle')
    bounds = await slider.get_bounds_using_js()

    start_x = bounds['x'] + bounds['width'] / 2
    start_y = bounds['y'] + bounds['height'] / 2
    end_x = start_x + 200  # Drag 200px to the right

    await tab.mouse.drag(start_x, start_y, end_x, start_y)

Hover Over Elements

async def hover_menu(tab):
    menu = await tab.find(css_selector='.dropdown-trigger')
    bounds = await menu.get_bounds_using_js()

    await tab.mouse.move(
        bounds['x'] + bounds['width'] / 2,
        bounds['y'] + bounds['height'] / 2,
    )
    # Menu should now be visible via CSS :hover

Learn More