Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Pass punctuation, symbols, accents, and Unicode to Puppeteer as ordinary JavaScript text. Focus the intended control, then give the complete value to locator().fill(), Keyboard.type(), or Frame.type(). Use Keyboard.press() for named keys such as Enter, Escape, arrows, and Control. Selector escaping and text handling are separate problems.

That distinction prevents most failures: a percent sign does not need Puppeteer-specific escaping, while a dynamically assembled CSS selector may need careful construction. The sections below show the event behavior, reliable patterns, edge cases, and a complete runnable example.

Use literal text for punctuation and Unicode

Keyboard.type(text) receives a string and emits keyboard events for each character. The current Puppeteer API describes the sequence as keydown, keypress/input, and keyup. Therefore characters such as %, &, €, em dashes, accents, and emoji belong directly in the value.

const value = 'Café — 50% & €';

await page.locator('input[name="query"]').fill(value);
// Or, after focusing the element:
await page.keyboard.type(value);

fill() sets the focused form control to the supplied value; keyboard.type() simulates typing. Choose the latter when the application depends on per-character keyboard events. Neither API requires an extra escape layer for ordinary punctuation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Escape only the JavaScript literal when necessary

JavaScript still has its own string-literal rules. Escape a quote that would terminate the literal, and use JavaScript escapes for control characters or code points when that is clearer:

const quote = 'She said 'ready'';
const line = 'first linensecond line';
const symbol = 'u{1F600}'; // 😀
await page.locator('#message').fill(`${quote}: ${line} ${symbol}`);

Those are JavaScript rules, not Puppeteer rules. A value read from JSON, a database, or an HTTP request should remain data and be passed as the text argument without manually backslash-escaping every symbol.

Use Keyboard.press() for named keys

Keys that have browser keyboard names are different from literal characters. The API documentation specifically directs callers to Keyboard.press() for keys such as Control and ArrowDown.

await page.locator('input[name="query"]').click();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Backspace');

press() can also take a text option when an input event must be forced for a named key. This is useful only when the page requires that narrower behavior; for normal text, use type() or fill().

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Shortcuts require explicit modifier state

For a shortcut, hold the modifier, press the named key, and release the modifier:

await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');

Use the modifier name appropriate to the browser and operating-system context. Puppeteer’s API index also references a macOS limitation involving the Command-A shortcut (issue 1313). If a Command-A sequence behaves differently on macOS, test the exact Puppeteer and browser versions you run and consider selecting text through the page’s own controls rather than assuming a desktop shortcut will be identical.

Holding Shift does not change Keyboard.type()

The API reference explicitly states that modifier keys do not affect keyboard.type: holding Shift will not convert the supplied string to uppercase. Send the uppercase text as the value, or use explicit key events when the application truly needs a Shift keydown.

await page.keyboard.down('Shift');
await page.keyboard.press('1'); // produces the shifted key behavior where supported
await page.keyboard.up('Shift');

// For text, provide the intended characters directly:
await page.keyboard.type('ABC');

Keep selectors separate from input values

Frame.type(selector, text) accepts a selector and a text value as separate arguments. Treat them as two different data flows: construct or validate the selector independently, then pass the complete value as text.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const selector = 'input[name="query"]';
const userValue = 'A/B? C&D = 100%';
await page.locator(selector).fill(userValue);

// The frame API has the same separation:
const frame = page.frames().find(f => f.url().includes('/search'));
if (!frame) throw new Error('Search frame was not found');
await frame.type('input[name="query"]', userValue);

Do not interpolate a user’s value into a selector merely because it contains punctuation. A value such as A/B? C&D is harmless as text but can make a selector invalid if it is inserted into CSS syntax. Prefer a stable attribute selector or a locator tied to the element, and keep the user value in a variable.

Choose the API by the event behavior you need

API Best use Event behavior Special-character rule
locator().fill() Set a form control reliably Sets the control’s value through the locator workflow Pass the literal string; no Puppeteer escaping
Keyboard.type(text) Simulate typing a string keydown, keypress/input, and keyup for each character Symbols and Unicode remain ordinary text
Keyboard.press(key) Enter, arrows, Escape, Control, and other named keys Key action for one named key; a text option can force an input event Use the key name, not a quoted character sequence
Keyboard.down() / up() Explicit modifier state or key timing Separate keydown and keyup control Useful when application code observes key state
Keyboard.sendCharacter() A narrower text event sequence Dispatches keypress and input without keydown or keyup Use only when the page expects that reduced sequence

When an application listens for a particular event, match the API to that listener instead of trying random escaping. For example, a widget that reacts to keydown needs type() or explicit down()/up(); a custom editor that only needs an input event may be compatible with sendCharacter().

Complete Puppeteer example

The following Node.js script creates a local form, enters accents and symbols, uses a named key, and verifies the resulting value. It avoids a network dependency, so it can be used as a minimal diagnostic.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  try {
    const page = await browser.newPage();
    await page.setContent(`
      <form id="form">
        <label>Query <input name="query" /></label>
        <button type="submit">Search</button>
      </form>
      <output id="result"></output>
      <script>
        form.addEventListener('submit', event => {
          event.preventDefault();
          result.textContent = form.query.value;
        });
      </script>`);

    const value = 'Café — 50% & € / 東京';
    const input = page.locator('input[name="query"]');
    await input.fill(value);
    await page.keyboard.press('Enter');

    await page.waitForSelector('#result');
    const received = await page.locator('#result').evaluate(el => el.textContent);
    if (received !== value) {
      throw new Error(`Value mismatch: ${JSON.stringify(received)}`);
    }
    console.log('Received:', received);
  } finally {
    await browser.close();
  }
})();

Install Puppeteer in the project that runs this script, save it as special-characters.js, and execute it with Node.js. If your application needs real per-character events rather than a direct fill, replace await input.fill(value) with await input.click(); await page.keyboard.type(value).

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handling frames, delayed controls, and custom editors

Inputs inside an iframe

Locate the correct frame first, then use the frame’s selector-and-text API. Waiting for the frame and its input prevents a valid value from being sent to a detached document.

const frame = page.frames().find(f => f.name() === 'checkout');
if (!frame) throw new Error('checkout frame is unavailable');
await frame.waitForSelector('input[name="cardholder"]');
await frame.type('input[name="cardholder"]', 'Zoë & Co.');

Contenteditable and rich-text editors

A contenteditable editor may not behave like a normal input. Focus it explicitly, then type the literal value and inspect the editor’s DOM or application state. Use down() and up() only when the editor’s keyboard handlers require modifier state.

Delayed or replaced elements

Modern applications often replace an input after rendering. Acquire the locator close to the action, wait for the control to be available, and avoid retaining an element handle across a re-render. If text appears to vanish, verify that the application did not replace the node after your action.

Troubleshooting special-character failures

The symbol is missing or the value is truncated

  • Confirm that the intended element is focused and that the selector matches the visible control rather than a hidden duplicate.
  • Log the JavaScript value before sending it and compare it with the control’s value afterward.
  • Try fill() to distinguish a value-setting problem from an event-sequence problem; then switch to keyboard.type() if the page requires typing events.
  • Check whether the application sanitizes, normalizes, or truncates input after Puppeteer has entered it.

Enter, arrows, or Escape are inserted as text

Use keyboard.press('Enter'), keyboard.press('ArrowDown'), or keyboard.press('Escape'). Do not pass the words “Enter” or “ArrowDown” to keyboard.type(); that API is for a text string.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Shift did not capitalize the value

This is expected. Modifier state does not transform the string passed to keyboard.type(). Supply uppercase characters directly, or send explicit keydown, key press, and keyup events for a shortcut-like interaction.

A selector fails only for certain values

The value has probably been mixed into selector syntax. Keep a fixed selector such as input[name="query"], or safely construct a selector independently of the text. Never “escape” the user value as a workaround and then pass that altered value to the input.

A shortcut works on one operating system but not another

Modifier names and browser keyboard behavior can differ. The Puppeteer API index calls out a macOS Command-A limitation (issue 1313). Reproduce the shortcut on the same OS, browser, and Puppeteer version used in production, and use an application-level selection method when the shortcut is not dependable.

The page reacts to the wrong events

Choose the event sequence deliberately. type() emits the full per-character sequence; sendCharacter() omits keydown and keyup; down() and up() let you control key state. Instrument the page’s event listeners or inspect its model to identify which sequence it expects.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability, performance, and test design

  • Make focus explicit: click or use a locator action immediately before keyboard input.
  • Use deterministic values: keep test strings in variables so logs, assertions, and retries use exactly the same Unicode sequence.
  • Assert the result: read the control’s value or the application state after entry; a completed Puppeteer command does not prove that business logic accepted the text.
  • Prefer the narrowest event sequence: use fill() for a value-only test, type() for realistic typing handlers, and lower-level methods only when a specific listener requires them.
  • Do not infer a success rate: official Puppeteer references document API behavior but publish no numeric benchmark for special-character success. Measure your own application and browser matrix if that metric matters.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than interactive keyboard testing, ScreenshotNeo makes one request to capture it. It accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.

Use the API documentation at https://screenshotneo.com/docs/ for all options, including full-page lazy-image loading, CSS-selector element capture, dark mode, device presets, retina scale, PDF paper and page-range controls, custom CSS or JavaScript, clicks before capture, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.

cURL

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

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Sign up for the free ScreenshotNeo plan.

Frequently Asked Questions

Are there published benchmarks for special-character success in Puppeteer?

The official API references document event and key behavior but do not publish a numeric success-rate statistic for special-character entry. Measure the browsers, operating systems, and editors used by your own application.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How can I prove that an application accepted the characters rather than merely displaying them?

Assert the control’s value or the application’s submitted model after the action, and test the submit or change handler that consumes it. A completed keyboard command alone is not an application-level assertion.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.