How to Run JavaScript in an Iframe with Puppeteer
Use Puppeteer’s Frame API to run JavaScript inside an iframe. Learn how to find the right frame, wait for content, pass values, and handle navigation.
To run JavaScript inside an iframe with Puppeteer, get the iframe’s Frame and call frame.evaluate(). page.evaluate() runs in the main page frame, so it will not find elements that exist only inside an iframe.
const iframeElement = await page.waitForSelector('iframe#app-frame');
const frame = await iframeElement.contentFrame();
if (!frame) throw new Error('Iframe frame was not available');
await frame.waitForSelector('#status');
const status = await frame.evaluate(() => {
return document.querySelector('#status')?.textContent?.trim() ?? null;
});
console.log(status);
This pattern waits for the iframe element, resolves its frame, waits for content inside that frame, then evaluates browser-side JavaScript there. Puppeteer describes Frame.evaluate() as behaving like Page.evaluate() except that it runs in the frame’s context. Puppeteer Frame.evaluate documentation.
1. Set up Puppeteer
In a Node.js project, install Puppeteer if it is not already a dependency:
npm install puppeteer
The following complete example launches a browser, opens a page, locates an iframe, reads text from inside it, and closes the browser:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const iframeElement = await page.waitForSelector('iframe#app-frame');
const frame = await iframeElement.contentFrame();
if (!frame) throw new Error('Could not resolve iframe to a frame');
await frame.waitForSelector('#status');
const status = await frame.$eval('#status', el => el.textContent.trim());
console.log(status);
} finally {
await browser.close();
}
})();
Replace the example URL and selectors with the page and iframe you control. This example assumes the page has an iframe matching iframe#app-frame and that the iframe eventually contains #status.
2. Find the iframe and get its Frame
An iframe element is an element in the parent document. Puppeteer’s ElementHandle.contentFrame() resolves the frame associated with that element. Use it when a selector can identify the iframe reliably.
const iframeElement = await page.waitForSelector('iframe#app-frame');
const frame = await iframeElement.contentFrame();
if (!frame) throw new Error('Iframe frame was not available');
The method can return null if the handle does not correspond to an iframe with an available frame. Check the result before calling frame methods. See ElementHandle.contentFrame().
Choose from the page’s frame list
If you know a frame’s URL or need to inspect the frame tree, use page.frames(). This includes the main frame and child frames.
const frames = page.frames();
for (const frame of frames) {
console.log({ url: frame.url(), name: frame.name() });
}
const targetFrame = frames.find(frame => frame.url().includes('/embedded-app/'));
if (!targetFrame) throw new Error('Target frame was not found');
const title = await targetFrame.evaluate(() => document.title);
console.log(title);
Use URL matching only when the URL is a dependable identifier for your application. A frame URL can change during navigation, and multiple frames can have similar URLs. You can also traverse from page.mainFrame() through each frame’s childFrames() when the nesting structure matters. The Page.frames() and Frame API references document these methods.
| Method | Good fit | Watch for |
|---|---|---|
contentFrame() |
You can identify the iframe element with a stable selector. | The selector must match the intended iframe; check for a null frame. |
page.frames() |
The frame URL or frame properties are the best way to identify it. | URLs may change and nested pages may contain several candidates. |
| Frame tree traversal | You need a particular parent-child relationship. | Nested frames are separate contexts; choose the specific child frame to evaluate. |
3. Evaluate JavaScript in the frame
Once you have the correct Frame, use its evaluate() method. The function runs in the browser’s context for that frame:
const result = await frame.evaluate(() => {
return {
title: document.title,
url: location.href,
status: document.querySelector('#status')?.textContent?.trim() ?? null,
};
});
console.log(result);
For a single matching element, frame.$eval(selector, fn) runs the callback with the first matching element:
const status = await frame.$eval('#status', element => element.textContent.trim());
If the element might not exist, first wait for it or use frame.evaluate() with a null-safe query. $eval() throws when there is no matching element.
Pass Node.js values as arguments
The function passed to evaluate() is serialized and executed in the browser. It cannot read variables or helper functions that exist only in the surrounding Node.js scope. Pass data as additional arguments instead:
const label = 'iframe title';
const result = await frame.evaluate((prefix) => {
return `${prefix}: ${document.title}`;
}, label);
console.log(result);
Pass each needed value explicitly. Keep the evaluated function self-contained, or define its browser-side helpers inside that function.
Return values and browser objects
Puppeteer waits for a promise returned by the evaluated function and transfers the resolved result to Node.js. Primitive values and ordinary serializable objects are suitable return values. A DOM node does not become a live Node.js DOM object when returned this way. If you need to keep and work with a browser-side object, use an evaluation handle and dispose of it when finished.
const handle = await frame.evaluateHandle(() => document.querySelector('#status'));
try {
console.log(await handle.jsonValue());
} finally {
await handle.dispose();
}
For element operations, a selector method such as frame.$eval() is often simpler. See Frame.evaluateHandle() for handle behavior.
4. Wait for the iframe’s content
Finding the iframe does not mean its document is ready or that an application has rendered the element you need. Wait for the relevant selector in the frame before reading it:
const frame = await iframeElement.contentFrame();
if (!frame) throw new Error('Iframe frame was not available');
await frame.waitForSelector('#status', { visible: true, timeout: 15000 });
const status = await frame.$eval('#status', el => el.textContent.trim());
frame.waitForSelector() works across navigations, which is useful when the iframe loads or replaces its document after the iframe element appears. Choose a timeout appropriate to the application and handle a timeout when a missing element is an expected possibility. See Frame.waitForSelector().
Frames that navigate, attach, or detach
Frames can attach, navigate, or detach while automation is running. If the iframe undergoes a significant navigation, reacquire the iframe’s current frame before continuing. A previously held frame can refer to a document that has changed or can become detached.
await page.waitForSelector('iframe#app-frame');
let frame = await (await page.$('iframe#app-frame')).contentFrame();
if (!frame) throw new Error('Iframe frame was not available');
await frame.waitForSelector('#ready');
// If your flow causes the iframe to navigate, resolve the current frame again.
const currentIframe = await page.$('iframe#app-frame');
frame = currentIframe ? await currentIframe.contentFrame() : null;
if (!frame) throw new Error('Iframe frame is no longer available');
const value = await frame.evaluate(() => document.querySelector('#ready')?.textContent ?? null);
In production code, check that the iframe element handle exists before calling contentFrame(), and ensure the expected page state has been reached after navigation. Avoid arbitrary sleeps where a selector or other explicit condition can express readiness.
5. Handle nested iframes
Each nested iframe has its own Frame. Evaluating in a parent frame does not automatically switch into its child. Resolve the inner iframe from its parent frame, then evaluate in the inner frame:
const outerElement = await page.waitForSelector('iframe#outer');
const outerFrame = await outerElement.contentFrame();
if (!outerFrame) throw new Error('Outer frame was not available');
const innerElement = await outerFrame.waitForSelector('iframe#inner');
const innerFrame = await innerElement.contentFrame();
if (!innerFrame) throw new Error('Inner frame was not available');
await innerFrame.waitForSelector('#result');
const result = await innerFrame.$eval('#result', el => el.textContent.trim());
console.log(result);
Target the frame that actually owns the element or code you need. A selector evaluated in the outer frame cannot find content belonging to the inner one.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
page.$eval() says the element is missing |
The element lives inside an iframe, outside the main document. | Find the iframe, resolve its Frame, then use frame.$eval() or frame.evaluate(). |
contentFrame() returns null |
The element handle is not an iframe or its frame is not available yet. | Confirm the selector targets an iframe, wait for it, and check the returned value before use. |
waitForSelector() times out |
The selector is wrong, the application has not rendered, or the wrong frame was selected. | Log page.frames() URLs, verify the selector in the correct document, and wait for the app’s actual ready element. |
| Evaluation reports that the frame was detached | The iframe was removed or replaced during navigation or a page update. | Wait for the update to complete, find the iframe element again, and resolve a fresh frame. |
A Node.js variable is undefined inside evaluate() |
The evaluated function runs in the browser and cannot close over Node.js variables. | Pass the value as an argument: frame.evaluate((value) => ..., value). |
| A returned element cannot be used like a Node.js DOM node | DOM nodes are not transferred as live objects by ordinary evaluation results. | Return serializable data, use $eval(), or use an evaluation handle for browser-side objects. |
| Code runs but sees the wrong document | The frame selection matched a different iframe, or a navigation changed the target. | Inspect frame URLs and names, use a more specific selector, and reacquire after navigation. |
| Nested content is missing | The code is running in the parent frame rather than the nested child. | Resolve each iframe level and evaluate in the frame that contains the target. |
7. Performance and reliability
- Wait for a condition, not a fixed delay. Waiting for the target selector usually avoids unnecessary idle time and reduces race conditions.
- Use a specific frame locator. Resolving the intended iframe directly avoids accidentally evaluating in a sibling frame.
- Keep evaluation work focused. Return only the data Node.js needs instead of copying large page structures across the browser boundary.
- Reacquire after navigation or replacement. Frame lifetimes follow the page’s iframe structure; handle detachment as a normal lifecycle event in dynamic pages.
- Make timeouts explicit. A bounded wait gives a clear failure point when the expected application state never appears.
- Close resources. Close the browser in a
finallyblock so failures do not leave browser processes running.
This task has no separate per-evaluation charge in the code shown; its practical cost is the browser time and compute used by your automation environment. The dossier contains no benchmark figures, so throughput should be measured against your own page, concurrency, and infrastructure.
Or skip the browser setup
If the goal is to capture a page or inspect how it renders rather than automate code inside a particular iframe, ScreenshotNeo provides a website screenshot API and MCP server. Its API makes a screenshot request with one GET call; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, no card required.
FAQ
Why does page.evaluate() not see elements inside my iframe?
It evaluates in the main frame. Resolve the iframe’s Frame and call frame.evaluate() in that context.
Can I use a value created in Node.js inside frame.evaluate()?
Yes. Pass it as an argument to the evaluation function; Node.js lexical variables are not available inside the browser callback automatically.
Does evaluating in an outer frame also evaluate in nested frames?
No. Each iframe is a separate frame context. Resolve the nested iframe and evaluate in its frame.
What should I return from an evaluation?
Return primitives or serializable data such as a small object. Use an evaluation handle if you need a live browser-side object.


