How to Capture a Puppeteer Screenshot After Dismissing a Modal
Dismiss a modal reliably in Puppeteer by waiting for the right UI state, then capture the page. Includes runnable examples and fixes for common failures.
To capture a Puppeteer screenshot after dismissing a modal, click the modal’s close control, wait for the modal to become hidden or detach, and only then call page.screenshot(). Use selectors that match the actual page; there is no universal modal or close-button selector.
const close = page.locator('[data-testid="modal-close"]'); // Replace with a real selector.
await close.click();
await page.waitForSelector('[role="dialog"]', { hidden: true }); // Replace with the modal root.
await page.screenshot({ path: 'after-dismiss.png' });
The example uses a Puppeteer Locator for the close action and an explicit hidden-state check for the modal. If the close action navigates instead of simply closing the dialog, coordinate the click with a navigation wait as shown below.
1. Identify the modal and its close control
Inspect the target page’s DOM and identify two selectors: one for the control that dismisses the modal, and one for the modal root whose disappearance confirms that the action worked. Prefer stable attributes such as a test ID or an accessible label when the page provides them. A selector such as [role="dialog"] button[aria-label="Close"] is only an example; verify that the target page actually uses it.
If the modal is inside an iframe, find the relevant Puppeteer Frame and perform the selector wait and click in that frame. Page-level selectors do not reach into an iframe’s document.
2. Capture after a modal closes
This complete Node.js script launches Chromium, opens a page, waits for a visible close button, clicks it, confirms the modal is hidden or detached, saves a screenshot, and closes the browser. Replace the URL and selectors for your page.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const modalSelector = '[role="dialog"]';
const closeSelector = '[role="dialog"] button[aria-label="Close"]';
await page.waitForSelector(closeSelector, {
visible: true,
timeout: 30_000,
});
await page.locator(closeSelector).click();
await page.waitForSelector(modalSelector, {
hidden: true,
timeout: 10_000,
});
await page.screenshot({ path: 'after-dismiss.png', fullPage: true });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Install Puppeteer in your project with npm install puppeteer if it is not already installed. The code uses CommonJS; in an ES module project, import Puppeteer with import puppeteer from 'puppeteer';. The screenshot guide documents page screenshots and element-only screenshots through ElementHandle.screenshot() (Puppeteer screenshot guide).
Locator or waitForSelector?
| Approach | Use it when | State confirmation |
|---|---|---|
| Locator | You want the action to wait automatically for its target and actionability. | Still wait for the modal root to be hidden or detached if you need to confirm dismissal before capture. |
waitForSelector plus page.click |
You want explicit, separate waits for a selector’s visibility and the modal’s disappearance. | Wait for the close control to be visible, click it, then wait for the modal root with { hidden: true }. |
Puppeteer documents Locator interaction and its waiting behavior in the page interactions guide. waitForSelector is a lower-level wait; a successful selector wait does not guarantee a later click will succeed if the page changes between steps.
3. Handle navigation or animation
If clicking the control navigates
Set up the navigation wait before clicking so the event is not missed. Puppeteer’s documented pattern is to start both promises together:
const [response] = await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click('[data-testid="modal-continue"]'),
]);
await page.screenshot({ path: 'after-navigation.png' });
Use the close or continue selector that actually triggers navigation. If the control only hides the modal, wait for the modal’s hidden or detached state instead. See the Puppeteer Page API for navigation coordination.
If the modal animates out
Waiting for the modal root to become hidden or detach is more reliable than sleeping for a guessed duration. If the site keeps the root visible while animating, wait for a stable post-dismissal condition that reflects the page’s behavior, such as the modal’s completed state or an overlay becoming hidden. A fixed delay can be used only when the page exposes no dependable state to wait for; it may be too short on a slow run and unnecessarily long on a fast one.
4. Screenshot options to choose
pathwrites the captured image to a file, as in the examples.fullPage: truecaptures the full page instead of only the visible viewport.- For a screenshot of a particular element, locate the element and use its element screenshot method rather than capturing the whole page.
Choose the capture size and screenshot mode based on what you need to inspect or store. See the official screenshots guide for the documented screenshot APIs.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Selector wait times out | The selector does not match this page, the modal has not appeared, or the control is in an iframe. | Verify the live DOM and accessible labels; wait for the correct state; use the corresponding frame when the modal is inside an iframe. |
| Click fails or hits the wrong target | The selector matches a different element, the control is not actionable, or the page changed after the wait. | Use a more specific selector and a Locator, then inspect whether an overlay or another element is intercepting the action. |
| Screenshot still contains the modal | The code captured immediately after the click without confirming dismissal, or it waited on the wrong modal root. | Wait for the actual modal root to become hidden or detach before capturing. |
| Navigation wait hangs or times out | The click closes the modal without navigating, or the chosen navigation condition does not occur. | For a non-navigating close, wait for the modal state. If navigation is expected, start the navigation wait and click together and choose an appropriate load condition. |
| Capture runs before the page looks ready | The modal disappeared but the underlying page still has content to load. | After dismissal, wait for a meaningful page-specific element or state before taking the screenshot. |
The documented default timeout for Page.waitForSelector() is 30 seconds and can be changed. Set explicit timeouts for the page and interaction conditions that matter to your job, so failures surface as useful errors rather than indefinite waits. See Page.waitForSelector().
6. Reliability, performance, and cost
State-based waits make capture timing depend on the page condition you care about instead of a guessed sleep. Keep selectors specific and use a modal root that represents the actual dismissal outcome. Always close the browser in a finally block so exceptions do not leave browser processes running.
For performance, wait for the smallest reliable condition: a modal’s hidden state or the specific post-dismissal content needed for the screenshot. Waiting for broader page-load events can take longer than necessary when the page continues background activity. A full-page screenshot may also take longer and produce a larger image than a viewport capture. Puppeteer itself is software; the cited documentation establishes no fixed runtime or cost figure, so measure these choices in your own workload.
Or skip the browser setup
If you need a screenshot without maintaining a browser script, ScreenshotNeo is a website screenshot API and MCP server. It does not provide a Puppeteer selector-click workflow for an arbitrary modal; it handles known consent banners, newsletter popups, and chat widgets before capture.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its 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. Sign up for free and get 1,000 screenshots a month with no card.
FAQ
Can I use a CSS selector for any modal?
Only if it matches that site’s DOM. Inspect the page and choose selectors for its real close control and modal root.
Should I wait for the modal to be hidden or detached?
Either can represent dismissal. Use the state that matches how the page removes or hides its modal.
Can the close button trigger navigation?
Yes. If it does, start waitForNavigation() and the click together with Promise.all before capturing the new page.


