ScreenshotNeo

BlogHow-to

How to Use Puppeteer Locators in an Iframe

Find the right Puppeteer Frame, create a locator inside it, and handle nested frames, waits, timeouts, and common iframe failures.

By the ScreenshotNeo team4 October 20269 min read

Get the Puppeteer Frame for the iframe, then create the locator from that frame: frame.locator(selector). A locator created from page searches the main frame; it does not automatically cross into iframe contents.

const frame = page.frames().find(candidate => candidate.url().includes('/embedded-form'));
if (!frame) throw new Error('Form iframe not found');

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

The example assumes the iframe URL has a distinctive fragment. In production, identify the intended frame using a dependable property of your page and verify that it exists before acting. Puppeteer’s Frame.locator() reference and page interactions guide document the API and locator behavior.

1. Install Puppeteer and run a complete example

Use a current Puppeteer installation in a Node.js project. Puppeteer’s standard package downloads a compatible Chrome for Testing browser during installation.

npm install puppeteer

Save the following as iframe-locator.mjs, replacing the example page and frame URL fragment with values from your application:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/contact', {
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  });

  // Choose a distinctive part of the expected iframe URL.
  const frame = page.frames().find(candidate =>
    candidate.url().includes('/embedded-form')
  );
  if (!frame) {
    const urls = page.frames().map(candidate => candidate.url());
    throw new Error(`Form iframe not found. Frames: ${urls.join(', ')}`);
  }

  await frame.locator('input[name="email"]').fill('reader@example.com');
  await frame.locator('button[type="submit"]').click();
  await frame.locator('[role="status"]').wait();
} finally {
  await browser.close();
}

Run it with node iframe-locator.mjs. The page URL, frame URL fragment, field selectors, and success condition are examples; use the actual values and expected result for your site. Puppeteer describes locators as its recommended way to select and interact with elements, with automatic waits for presence and action preconditions.

2. Find the intended iframe

A page has a main frame and may have child frames, including nested frames. page.frames() returns the current frame tree as a flat list; page.mainFrame() and frame.childFrames() let you walk it by parent and child. Each frame has its own document context.

Find by a distinctive frame URL

const frame = page.frames().find(candidate =>
  candidate.url().includes('/checkout')
);
if (!frame) throw new Error('Checkout frame not found');
await frame.locator('button[type="submit"]').click();

Use a fragment that distinguishes the target from other frames. A broad match such as includes('example.com') can select the wrong frame when a page contains several embedded applications.

Inspect the frame tree when you are unsure

function printFrames(parent, depth = 0) {
  for (const child of parent.childFrames()) {
    console.log(`${'  '.repeat(depth)}url=${child.url()}`);
    printFrames(child, depth + 1);
  }
}

console.log(`main=${page.mainFrame().url()}`);
printFrames(page.mainFrame());

This is useful for discovering the URL structure and nesting during development. Avoid relying on frame-array positions such as page.frames()[1]: advertisements, analytics, or page updates can change the order.

Nested iframe

If the target is nested, first find its parent frame, then inspect that frame’s children. A locator searches only within the frame from which it was created.

const parent = page.frames().find(candidate =>
  candidate.url().includes('/widget-shell')
);
if (!parent) throw new Error('Widget parent frame not found');

const child = parent.childFrames().find(candidate =>
  candidate.url().includes('/embedded-form')
);
if (!child) throw new Error('Nested form frame not found');

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

3. Create and use a locator in the frame

Call frame.locator(selector) and then use a supported action such as click(), fill(), hover(), or wait(). Locator actions wait for the element and relevant readiness conditions. For actions such as clicking, Puppeteer checks conditions including viewport position, visibility, enabled state, and a stable bounding box.

const email = frame.locator('input[name="email"]');
await email.fill('reader@example.com');
await frame.locator('input[name="subscribe"]').fill(true);
await frame.locator('button[type="submit"]').click();
await frame.locator('[role="status"]').wait();

fill() works with inputs, textareas, selects, contenteditable elements, and boolean values for checkboxes, radio buttons, and switches, as described in Puppeteer’s interaction guide. Use the action that matches the element: for example, fill a text input rather than simulate typing one key at a time when no key-by-key behavior is needed.

4. Choose a selector that survives page changes

CSS selectors work directly. Puppeteer also supports selector syntax for text, accessibility role and name, XPath, and combinations that can pierce supported shadow roots. Prefer an accessible name or stable application attribute when available; selectors tied to generated class names or incidental DOM structure may break when the page changes.

// CSS selector
await frame.locator('button[type="submit"]').click();

// Accessible name and role
await frame.locator('::-p-aria([name="Continue"][role="button"])').click();

// Text selector
await frame.locator('::-p-text(Continue)').click();

// XPath selector
await frame.locator('::-p-xpath(//button[@type="submit"])').click();

Use the selector form supported by your installed Puppeteer version and make sure it identifies the intended element within that frame. A selector that works on the main document may still match nothing inside the iframe.

5. Waits, timeouts, and frame lifecycle

Locators retry while waiting for the target and required action conditions. By default, their timeout comes from the page’s timeout settings; set a per-locator timeout when a particular iframe action needs a different limit.

await frame.locator('button[type="submit"]')
  .setTimeout(10_000)
  .click();

If the iframe loads after the top-level page, wait for the frame to appear before looking it up. Polling also handles applications that attach the iframe asynchronously:

async function waitForFrame(page, urlPart, timeoutMs = 15_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const match = page.frames().find(candidate =>
      candidate.url().includes(urlPart)
    );
    if (match) return match;
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  throw new Error(`Timed out waiting for frame containing ${urlPart}`);
}

const frame = await waitForFrame(page, '/embedded-form');
await frame.locator('input[name="email"]').fill('reader@example.com');

Frames can attach, navigate, or detach as the page changes. If the application replaces an iframe during a flow, a previously found frame may no longer be the active target. Look up the frame again after a known replacement or navigation, then retry the interaction under an explicit timeout. Avoid adding arbitrary long sleeps where waiting for the frame or target gives a clearer condition.

6. Fallback APIs when a locator does not fit

For an operation not covered by locators, use the frame’s lower-level query methods. frame.waitForSelector() waits for a selector and returns an ElementHandle or null depending on the options and result. Dispose of handles when finished.

const handle = await frame.waitForSelector('input[name="email"]', {
  visible: true,
  timeout: 10_000,
});
if (!handle) throw new Error('Email input not found');

try {
  await handle.type('reader@example.com');
} finally {
  await handle.dispose();
}

frame.$(selector) queries immediately and returns the first match or null; it does not provide locator-style waiting. Choose it when you intentionally want a one-time query and will handle the missing-element case.

const button = await frame.$('button[type="submit"]');
if (!button) throw new Error('Submit button not present');
try {
  await button.click();
} finally {
  await button.dispose();
}
Method Use it when Wait behavior
frame.locator() You want to interact with an element using a supported locator action. Waits for the element and the action’s readiness checks.
frame.waitForSelector() You need an element handle or a lower-level operation. Waits for the selector; you manage the handle and action.
frame.$() You expect an immediate query and want the first match or null. Does not wait for a future match.

7. Common errors and fixes

Symptom Likely cause Fix
Locator times out although the element is visible in the browser The locator was created from page or the wrong frame. Find the target frame, then call frame.locator(). Check the current frame URLs and nesting.
“Frame not found” The iframe has not attached yet, its URL differs from the assumed fragment, or it has been replaced. Inspect page.frames(), wait for the expected frame, and look it up again after a replacement.
Frame found but selector does not match The selector is wrong for the iframe’s document, or content is still loading. Inspect the frame URL and its content, confirm the selector within that frame, and wait for the target using a locator or waitForSelector().
Click times out while the element exists The locator’s action preconditions are not met: it may be hidden, disabled, outside the viewport, or moving. Check the element’s state and page layout. Wait for the application’s ready state and use a suitable locator action.
Works locally but not in CI Different load timing, browser setup, network conditions, or headless layout changes may expose a race or brittle selector. Wait on frame and element conditions, use stable selectors, and capture diagnostic frame URLs and errors in the failing run.
A saved ElementHandle becomes unusable The frame navigated or detached after the handle was created. Reacquire the current frame and element after navigation or replacement; dispose of old handles.

8. Reliability, performance, and cost considerations

  • Reliability: identify frames by meaningful URL or other dependable page-specific properties, check for a missing match, and account for attach, navigation, and detach events. Reacquire a frame after the application replaces it.
  • Performance: avoid repeated full-frame scans inside tight loops. Find the frame once for a stable interaction sequence, but look it up again after lifecycle changes. Use condition-based waits instead of long fixed delays.
  • Runtime cost: Puppeteer runs a browser process, so resource use depends on the page, browser, and workload. Close the browser in a finally block and avoid launching a new browser for every action when your application can safely reuse one.
  • Operational cost: account for browser hosting and execution in your own environment. Puppeteer’s API documentation does not provide a universal cost or speed benchmark for this iframe workflow.

9. Or skip the browser setup

If you only need a screenshot of the page or its rendered result, ScreenshotNeo is a website screenshot API and MCP server. It can return PNG, JPEG, WebP, or PDF from one GET request. For browser automation details and options, see the ScreenshotNeo documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/contact"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/contact',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with the outcome reported in X-Page-Verdict and X-Billed headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The Free plan includes 1,000 shots a month without a card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.

Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.

10. FAQ

Can a page locator select an element inside an iframe?

For iframe content, get its Puppeteer Frame and create the locator from that frame. This makes the selector run in the iframe’s document context.

Does a cross-origin iframe prevent Puppeteer from using a frame locator?

Use the frame context exposed by Puppeteer and select within it. The page’s browser-side JavaScript same-origin restrictions are a separate concern from Puppeteer’s frame APIs; the target frame still needs to be attached and available.

Can locators search inside a nested iframe automatically?

No. Find the nested frame through its parent’s childFrames(), then create the locator from that child frame.

When should I choose waitForSelector() instead?

Use it when you specifically need an ElementHandle or an operation the locator API does not offer. For ordinary selection and interaction, Puppeteer recommends locators.

References