ScreenshotNeo

BlogHow-to

How to Set an Input Value with Puppeteer

Learn the reliable ways to fill inputs, textareas, selects, and custom controls with Puppeteer, including events, selectors, waiting, errors, and fallbacks.

By the ScreenshotNeo team1 October 20267 min read

The current high-level way to set an input value in Puppeteer is a locator with fill():

await page.locator('input[name="email"]').fill('user@example.com');

locator.fill() waits for the element to be in the viewport, visible, enabled, and stable before acting. It detects the control type at runtime and supports inputs, textareas, selects, contenteditable elements, and boolean controls such as checkboxes, radio buttons, and switches. See the Puppeteer Locator.fill reference.

Complete Puppeteer example

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/form');

  await page.locator('input[name="email"]').fill('user@example.com');
  await page.locator('button[type="submit"]').click();
} finally {
  await browser.close();
}

The try/finally block closes Chromium even when navigation or form interaction fails. The launch, page creation, navigation, locator action, and cleanup sequence follows Puppeteer’s getting-started pattern.

Fill common form controls

Text inputs

await page.locator('#username').fill('alice');
await page.locator('input[name="email"]').fill('user@example.com');
await page.locator('input[type="search"]').fill('Puppeteer');

Textareas

await page.locator('textarea[name="message"]').fill('Hello from Puppeteer');

Select controls

await page.locator('select[name="country"]').fill('US');

fill() chooses the appropriate fill behavior for the control it finds, including a <select>.

Contenteditable elements

await page.locator('[contenteditable="true"]').fill('Editable text');

Checkboxes, radios, and switches

await page.locator('input[name="terms"]').fill(true);
await page.locator('input[name="marketing"]').fill(false);

Pass a boolean for checkbox, radio, or switch controls. Use a stable selector so the locator targets the intended control.

Selecting the right element

A reliable selector is usually tied to an ID, name, label, role, or accessible name. Broad selectors such as input become fragile when a page contains several fields.

await page.locator('#search').fill('Puppeteer');
await page.locator('input[name="email"]').fill('user@example.com');
await page.locator('::-p-aria(Search)').fill('Puppeteer');

The accessible-name form ::-p-aria(Search) is useful when the visible label or ARIA name is more stable than generated classes. Puppeteer also supports CSS, text, XPath, and other selector forms. A locator action retries until its action preconditions are met and can be given a per-locator timeout.

Label-based selection

If the page exposes an accessible label, prefer that relationship over positional selectors. For example, use an accessible-name locator when the form field is announced as “Search”. If there is no usable label or accessible name, use a stable id or name.

fill() versus page.type()

Approach Best for Events and behavior
locator.fill(value) Normal inputs, textareas, selects, contenteditable, and boolean toggles High-level type-aware filling with locator waiting and action checks
page.type(selector, text) Pages that depend on keyboard entry or per-character handlers Sends keydown, keypress/input, and keyup for each character
page.evaluate() Custom DOM operations and framework-specific workarounds Runs JavaScript in the page context; you must handle state and events yourself

Use page.type() for keyboard-driven behavior

await page.type('#username', 'alice');
await page.type('#username', ' slowly', {delay: 75});

The delay option sets the time between key presses and defaults to zero. Choose this method when the application formats text, validates each character, or triggers behavior specifically from keyboard events. See the Page.type reference.

Set a value with page evaluation

Direct DOM assignment is a lower-level escape hatch. It is useful for custom controls, but assigning element.value alone may not update application state. Dispatch the events the page expects:

await page.evaluate(({selector, value}) => {
  const element = document.querySelector(selector);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error('Expected an input element');
  }

  element.value = value;
  element.dispatchEvent(new Event('input', {bubbles: true}));
  element.dispatchEvent(new Event('change', {bubbles: true}));
}, {selector: '#username', value: 'alice'});

page.evaluate() runs the function in the page context and waits for a returned promise. For framework-controlled fields, the framework may require its own setter or event sequence. Keyboard-driven entry through page.type() is often the safer fallback when direct assignment does not update state. See the Page.evaluate reference.

Read the resulting value

const value = await page.$eval(
  '#username',
  (element) => (element instanceof HTMLInputElement ? element.value : '')
);
console.log(value);

$eval passes the first matching element to your function and throws when no element matches. In TypeScript, annotate the callback parameter as HTMLInputElement when needed.

Waiting for fields before filling

Locators automatically wait for viewport visibility, enabled state, and stable layout. This handles many pages where a form appears after navigation or animation:

const email = page.locator('input[name="email"]');
await email.fill('user@example.com');

If you need a lower-level handle for a custom operation, use waitForSelector:

const input = await page.waitForSelector('#username');
if (!input) throw new Error('Input not found');
await input.click();
await input.dispose();

waitForSelector waits for DOM availability only. It does not automatically retry a failed action, so a returned handle needs explicit interaction and disposal.

Forms that change after navigation or JavaScript

  • Navigate to the page before locating the field.
  • Use a locator that identifies the final control, not a temporary loading placeholder.
  • Let the locator wait for visibility and stable layout.
  • After filling, verify the value or submit and wait for the resulting navigation or application state.
await page.goto('https://example.com/form');
const email = page.locator('input[name="email"]');
await email.fill('user@example.com');
const entered = await page.$eval(
  'input[name="email"]',
  (element) => (element instanceof HTMLInputElement ? element.value : '')
);
if (entered !== 'user@example.com') {
  throw new Error(`Unexpected value: ${entered}`);
}

Troubleshooting

“No element found” or a timeout

Cause: The selector is wrong, matches nothing, or the control has not been added yet.

Fix: Inspect the final DOM, use an ID, name, label, role, or accessible-name selector, and avoid a broad selector such as input. If the field is rendered later, keep the locator action so Puppeteer can retry while its preconditions become true.

The field is found but cannot be filled

Cause: The element is hidden, disabled, moving, covered, or outside the viewport.

Fix: Wait for the page state that enables the field, then use locator.fill(); its action checks include visibility, enabled state, viewport presence, and layout stability.

Setting element.value does not update the form

Cause: A framework or custom component keeps its own state and is listening for input or change events.

Fix: Prefer fill() or page.type(). If you must assign the property directly, dispatch bubbling input and change events, then verify the value and application state.

Per-character validation does not run

Cause: Direct filling does not reproduce the exact keyboard sequence your page expects.

Fix: Use page.type(selector, text). Add a small delay only when the page requires paced input.

The wrong field is filled

Cause: The selector matches multiple controls or depends on generated class names.

Fix: Narrow it with a stable name, id, accessible name, or a form container.

waitForSelector returns a handle that becomes stale

Cause: A client-side render replaced the original DOM node.

Fix: Prefer a locator, which resolves and retries the action against the current element. If you use an ElementHandle, reacquire it after rerendering and dispose of handles when finished.

Performance and reliability

  • Reuse one browser process and create pages as needed instead of launching Chromium for every field.
  • Use stable selectors to reduce retries and failures after markup changes.
  • Use fill() for ordinary controls; reserve per-character typing for behavior that actually depends on keyboard events.
  • Keep typing delays at zero unless the application requires paced entry.
  • Close the browser in a finally block so failed runs do not leave Chromium processes behind.
  • Verify critical values after filling, especially when a component transforms or masks input.

Or skip the browser setup

If your goal is to capture the page after handling a form or to automate screenshots around a workflow, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/form -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/form"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/form'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));

There are 1,000 screenshots per month on the free plan with no card. Paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account.

FAQ

Use await page.locator(selector).fill(value). It is type-aware and waits for the element to be actionable.

Should I use fill() or type()?

Use fill() for ordinary form controls. Use type() when per-character keyboard and input events matter.

Can fill() set a select or checkbox?

Yes. It supports selects and accepts a boolean for checkbox, radio, and switch controls.

Why does direct assignment fail in React or another framework?

Frameworks can track state separately from the DOM property. Use locator filling or keyboard typing, or dispatch the required events and verify the resulting application state.

When should I use $eval?

Use it for a one-element read or custom operation when a locator action does not cover the task. It throws if no element matches.