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

2.22intermediate5 min read

Drag-and-Drop, Date Pickers, Complex Form Controls

The form controls that look custom because they are. Three patterns, drag-drop, custom date pickers, and rich select widgets, and how to drive them reliably.

What you’ll learn

  • Drive HTML5 drag-and-drop with Playwright's `drag_to` and manual mouse primitives.
  • Type into custom date pickers vs clicking the calendar UI.
  • Open and select from rich combobox widgets (typeahead, multi-select, virtualised lists).
  • Recognise when to bypass the UI entirely by setting input values directly.

Form controls that look fancy don't use the standard <select> or <input type="date">, they're built on top of <div> and <span> with custom JS. Your scraper can't select_option on a <div>. This lesson is the playbook for the three most common custom controls.

Drag-and-drop

Three implementations exist:

  1. HTML5 native drag-and-drop. Elements have draggable="true"; the page listens for dragstart, dragover, drop. The most common.
  2. Custom mouse-driven. Listens for mousedown, mousemove, mouseup and updates positions manually. Used by react-dnd, sortable.js, etc.
  3. Pointer events. Modern alternative; library-driven.

Playwright's drag_to handles #1 cleanly:

source = page.locator("[data-id='item-1']")
target = page.locator("#drop-zone")
source.drag_to(target)

For #2 and #3, you may need manual mouse primitives:

source = page.locator("[data-id='item-1']")
target = page.locator("[data-position='3']")

src_box = source.bounding_box()
tgt_box = target.bounding_box()

page.mouse.move(src_box["x"] + src_box["width"]/2, src_box["y"] + src_box["height"]/2)
page.mouse.down()
# Move through intermediate points, some libs require this
page.mouse.move(src_box["x"] + 50, src_box["y"] + 50)
page.mouse.move(tgt_box["x"] + tgt_box["width"]/2, tgt_box["y"] + tgt_box["height"]/2, steps=10)
page.mouse.up()

steps=10 interpolates the move into ten intermediate positions. Some drag libraries (sortable.js especially) require multiple mousemove events to register the drag, not a single jump.

Sanity-check after dropping: read the new state and verify the item moved. Drag-and-drop tests are notorious for silently failing.

Custom date pickers

Three subspecies, each with a different scraping strategy:

Native input[type="date"]

<input type="date" id="dob">

The browser supplies the calendar UI. Just fill:

page.locator("#dob").fill("2025-03-15")

ISO-8601 format. Works in headless mode because the browser doesn't actually render a calendar, it just stores the value.

Custom picker with a hidden input

<input type="text" id="dob" readonly>
<button class="open-picker">Pick date</button>
<!-- when clicked, opens a div-based calendar -->

The visible input is usually readonly, fill() may or may not work. Three strategies:

  1. Type into the input anyway (often works despite readonly):
page.locator("#dob").fill("2025-03-15")
page.locator("#dob").press("Enter")
  1. Click through the calendar UI:
page.locator(".open-picker").click()
page.locator(".calendar [data-month='March 2025']").wait_for()
page.locator(".calendar [data-day='15']").click()
  1. Set the value via JS (last resort):
page.evaluate("document.getElementById('dob').value = '2025-03-15'")
page.locator("#dob").dispatch_event("change")

Use this when neither the UI nor fill works. The change event is critical, most frameworks listen for it and won't see the new value otherwise.

Calendar widget with internal state

A React date picker stores the date in component state, not the input. You must trigger the picker's own event handlers:

page.locator(".date-input").click()  # open picker
page.locator(".react-datepicker__navigation--next").click()  # change month
page.locator("[aria-label='March 15th, 2025']").click()  # pick day

Find the right ARIA labels or test-IDs by inspecting the rendered picker. Each library (react-datepicker, mui-x-date-pickers, flatpickr) has its own structure.

Rich combobox / typeahead widgets

A <select> styled with JS. Common shapes:

  • react-select, typeahead with virtualised list.
  • headless-ui Combobox, accessible, role="combobox", role="option".
  • Custom div-based dropdowns, open with click, options as <li> or <div>.

For ARIA-compliant widgets (most React libraries are now):

# Open the combobox
page.get_by_role("combobox", name="Country").click()

# Type to filter
page.get_by_role("combobox", name="Country").fill("United")

# Wait for filtered option to appear, click
page.get_by_role("option", name="United States").click()

ARIA roles are the most stable selectors for these widgets because they're required by accessibility law in many jurisdictions, sites don't strip them.

For non-ARIA-compliant widgets:

page.locator(".dropdown-toggle").click()
page.locator(".dropdown-item:has-text('United States')").click()

Inspect the rendered HTML; find a stable text or attribute pattern; build the locator chain.

Multi-select

# Hold Ctrl/Cmd and click each option
for option in ["JavaScript", "Python", "Rust"]:
  page.locator("li", has_text=option).click(modifiers=["Control"])

Some widgets work via "click to toggle" instead, check by clicking one item and seeing whether others stay selected.

Sliders

slider = page.locator("input[type='range']")
slider.fill("50")  # native range input

Custom sliders (React, jQuery UI) need keyboard or pointer drag:

handle = page.locator(".slider-handle")
handle.focus()
for _ in range(20):
  handle.press("ArrowRight")

Or drag the handle. The keyboard approach is more deterministic for accessibility-compliant widgets, they respond to ArrowLeft/ArrowRight as required.

Toggles and switches

# Visible switch is a styled checkbox
page.locator("input[type='checkbox']").check()

# Custom toggle with role
page.get_by_role("switch", name="Enable notifications").click()

check() is smart, it verifies the checkbox state changed and retries once if not.

When to bypass the UI entirely

For every custom widget there's a hidden underlying input or state. Sometimes the right move is to set it directly and dispatch the framework's expected event:

page.evaluate("""({ selector, value }) => {
  const el = document.querySelector(selector);
  const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
  nativeSetter.call(el, value);
  el.dispatchEvent(new Event('input', { bubbles: true }));
}""", {"selector": "#dob", "value": "2025-03-15"})

This is the "React-friendly" way to programmatically set an input value: bypass React's value tracking via the native setter, then dispatch input. It's how playwright's fill works under the hood.

Use this when normal fill doesn't trigger React state updates, usually for libraries that wrap inputs with their own tracking.

Hands-on lab

Open /events. Find the date picker. Inspect its DOM, is it a native <input type="date">, a div-based custom picker, or a React widget? Pick the appropriate strategy from this lesson and submit a date filter. Verify the events listing changes to reflect your selection. Then visit /challenges/dynamic/date-picker/custom for a purpose-built test of a custom picker.

Hands-on lab

Practice this lesson on Catalog108, our first-party scraping sandbox.

Open lab target → /events

Quiz, check your understanding

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

Drag-and-Drop, Date Pickers, Complex Form Controls1 / 8

Which type of drag-and-drop is `source.drag_to(target)` MOST reliable for?

Score so far: 0 / 0