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.
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
finallyblock 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
What is the recommended Puppeteer API for a normal input?
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.


