How to Wait for a Specific Element Before Taking a Puppeteer Screenshot
Wait for the exact DOM state your screenshot needs with Puppeteer’s selector and function waits, then capture the page or element reliably.
Use page.waitForSelector() and await it before calling page.screenshot(). Add { visible: true } when the element must be visible. For an image of only that element, call screenshot() on the returned element handle.
const element = await page.waitForSelector('#target', {
visible: true,
timeout: 10_000,
});
if (!element) {
throw new Error('Target element was not found');
}
await page.screenshot({ path: 'page.png' });
// To capture only the selected node instead:
// await element.screenshot({ path: 'element.png' });
1. Complete runnable example
Install Puppeteer, save the following as capture.js, and run it with Node.js. The example waits for a visible element, captures the full page, and closes the browser even if the wait or capture fails.
npm install puppeteer
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 element = await page.waitForSelector('h1', {
visible: true,
timeout: 10_000,
});
if (!element) throw new Error('The target element was not found');
await page.screenshot({ path: 'page.png', fullPage: true });
// For only the element, use: await element.screenshot({ path: 'element.png' });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The example uses domcontentloaded so navigation need not wait for every resource. The selector wait is a separate readiness condition. Use the lifecycle wait your target site needs; navigation completion alone does not prove the desired element is ready.
2. Choose the readiness condition
| Need | Wait | Behavior |
|---|---|---|
| Element exists in the DOM | page.waitForSelector(selector) |
Resolves as soon as a match exists, including if it already existed when called. |
| Element is visible | page.waitForSelector(selector, { visible: true }) |
Requires a matching element that is not display: none or visibility: hidden. |
| Element is hidden or absent | page.waitForSelector(selector, { hidden: true }) |
Useful for waiting for a loading overlay to disappear. It may resolve with null when no match exists. |
| Application-specific state | page.waitForFunction() |
Repeatedly evaluates a condition until it returns a truthy value. |
Visibility does not mean the element’s data, images, fonts, or animation are finished. If the app exposes a reliable ready flag or attribute, wait for that explicitly. For example:
await page.waitForFunction(() => {
const target = document.querySelector('#target');
return target?.getAttribute('data-state') === 'ready';
}, { timeout: 15_000 });
await page.screenshot({ path: 'page.png' });
Use a condition the page actually exposes; do not assume a generic selector indicates that asynchronous rendering has completed.
3. Page screenshot or element screenshot
Call page.screenshot() to capture the page. Call element.screenshot() on the ElementHandle returned by waitForSelector() to capture only that node. The element screenshot method attempts to scroll the element into view if needed.
const target = await page.waitForSelector('.report-card', { visible: true });
if (!target) throw new Error('Report card not found');
await target.screenshot({ path: 'report-card.png' });
For full-page output, pass fullPage: true to the page screenshot. Be aware that lazy-loaded content farther down may not be present unless the page has loaded it; wait or scroll according to the application’s behavior.
4. Timeouts, defaults, and cancellation
The documented default selector timeout is 30,000 ms. Set a timeout for a particular wait, set a page-wide default with page.setDefaultTimeout(), or use timeout: 0 to disable the timeout. Disabling it can leave a capture job waiting forever if the selector never appears.
page.setDefaultTimeout(12_000);
const target = await page.waitForSelector('#target', {
visible: true,
timeout: 8_000, // overrides the page default for this wait
});
Selector wait options also support a cancellation signal. Use cancellation when the surrounding job can be abandoned, and handle cancellation as a normal job outcome. For a missing element, the wait rejects at timeout; catch the error if your workflow should record a failed capture or take another action.
5. Loading overlays, navigation, and frames
If a spinner must disappear before capture, wait for it to become hidden, then wait for the target to be ready:
await page.waitForSelector('.loading-overlay', {
hidden: true,
timeout: 20_000,
});
await page.waitForSelector('#results', {
visible: true,
timeout: 10_000,
});
await page.screenshot({ path: 'results.png' });
Navigation and target readiness are separate checks. If both matter, await navigation’s lifecycle condition and then the selector or application-ready condition. Puppeteer’s screenshot guide demonstrates waiting for navigation with networkidle2; choose it only when that network condition fits the site, since long-lived requests can prevent network idle.
For a target inside an iframe, wait on that frame rather than the main page:
const frame = page.frames().find((candidate) => candidate.url().includes('/embedded/'));
if (!frame) throw new Error('Embedded frame not found');
const target = await frame.waitForSelector('.ready', { visible: true, timeout: 10_000 });
if (!target) throw new Error('Target not found in frame');
await page.screenshot({ path: 'frame-page.png' });
The page screenshot still captures the rendered page; use the frame’s element handle screenshot if you want only the selected node.
6. Locator alternative
Puppeteer recommends locators for selecting and interacting with elements because they wait for presence and appropriate state. Use a locator when you are doing a broader interaction flow. The lower-level waitForSelector() remains useful when you want a direct selector wait and an ElementHandle for an element-only screenshot.
const locator = page.locator('#target');
await locator.wait();
await page.screenshot({ path: 'page.png' });
7. cURL, Python, and Node.js
Puppeteer is a Node.js library, so the wait itself runs in JavaScript. cURL and Python do not control a Puppeteer page or provide its selector wait. They can call a screenshot service that accepts capture instructions. For ScreenshotNeo’s supported options and exact parameters, see the ScreenshotNeo documentation.
Basic cURL request:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Basic Python request:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.write(r.content)
Basic Node.js request:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Selector wait times out | The selector is wrong, the element is in another frame, or the page never reaches the expected state. | Inspect the rendered DOM, check the selector spelling, locate the target frame, and verify the app state. Increase timeout only if the page legitimately needs longer. |
| Wait resolves but screenshot is incomplete | The element exists or is visible before its content or assets finish loading. | Wait for an app-specific ready attribute, data condition, or loading overlay to disappear. Visibility alone is not a content-ready signal. |
| Hidden wait resolves immediately | The selector was absent when the wait began. | That is valid hidden-state behavior. Use a presence or visible wait for the target, and hidden wait only for something expected to disappear. |
| Navigation wait hangs | The chosen network-idle condition may not occur because the page keeps requests active. | Choose an appropriate lifecycle condition, then wait for the specific target or app-ready state. |
| Element handle is null | A hidden wait can resolve with null; alternatively the expected match was not obtained. |
Check the result before calling screenshot(); use a visible wait for an element capture. |
| Screenshot misses iframe content | The selector wait was performed in the main frame while the element belongs to an iframe. | Find the correct frame and call its waitForSelector(). |
| Screenshot captures an animation mid-frame | The selector became visible while the animation was still running. | Wait for the application’s completed state or animation signal before capture. |
9. Performance, reliability, and cost
A selector wait usually avoids wasting time on a fixed sleep when the target appears sooner, while still allowing a timeout when it never appears. Keep waits scoped to a meaningful condition and use finite timeouts so a stuck page does not occupy a worker indefinitely. If navigation readiness and element readiness are both required, check both explicitly.
For repeatable captures, use stable selectors or app-owned readiness markers, close the browser in a finally block, and record timeout failures separately from successful screenshots. Puppeteer’s own cost depends on the compute and browser infrastructure running the script; the research sources provide no benchmark or universal cost figure.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It takes a URL in one request and returns an image or PDF. It does not expose Puppeteer’s arbitrary selector-wait workflow, so use Puppeteer when capture must wait on a page-specific selector. For URL-based captures, the request is:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Cookie banners are accepted and removed before the shot, along with supported newsletter popups and chat widgets. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the API docs, then sign up free.
11. FAQ
Does waitForSelector() wait for an element that is already on the page?
Yes. It resolves immediately if the selector already matches.
Should I use waitForSelector() or a locator?
Use locators for general interactions; use the selector wait when you need its direct wait behavior or returned element handle.
Does visible: true guarantee the screenshot is visually complete?
No. It checks visibility, not whether the application’s content or assets are finished.
Which screenshot method captures just the element?
Call screenshot() on the element handle returned by the selector wait.


