How to Hide Chat Widgets in Puppeteer Screenshots
Hide a chat widget before capturing a Puppeteer screenshot with a selector confirmed on the target site. Handle delayed widgets, iframes, and shadow roots.
To hide a chat widget in a Puppeteer screenshot, identify the widget’s selector on the target site, wait until it appears, hide only that element in the page, and then call page.screenshot(). Puppeteer has no universal chat-widget selector: a class name such as .chat-widget-container is only an example, not a Puppeteer feature.
The example below assumes the widget is in the page’s ordinary DOM and becomes visible before the wait times out. If it is in an iframe or shadow root, target that context instead. Puppeteer’s guide says, “For capturing screenshots use Page.screenshot().” See the Puppeteer screenshots guide and the ScreenshotOptions reference.
1. Identify the chat widget selector
Inspect the target page in browser developer tools and find an element that identifies the widget without matching unrelated page content. Check whether the visible bubble and its expanded panel are separate elements; you may need to hide both. Confirm the selector against the actual page before using it in automation.
A narrow selector limits the chance of removing legitimate content. For example, hiding a broad iframe, button, or aside selector could remove other parts of the page.
2. Hide the widget before capture
This complete example uses Node.js with Puppeteer. Replace the URL and selector with values for the page you are capturing.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
});
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
const selector = 'YOUR_CONFIRMED_WIDGET_SELECTOR';
await page.waitForSelector(selector, { visible: true, timeout: 10000 });
await page.evaluate((widgetSelector) => {
const widget = document.querySelector(widgetSelector);
if (widget) {
widget.style.setProperty('display', 'none', 'important');
}
}, selector);
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Install Puppeteer in your project with npm install puppeteer. The placeholder selector is deliberately not a working vendor selector: use one you have confirmed on the destination site. page.evaluate() executes its function in the page context, where document.querySelector() can find ordinary DOM elements.
Use an injected CSS rule instead
If the selector is stable, a CSS rule can be concise. Inject it after navigation and before capture:
await page.addStyleTag({
content: 'YOUR_CONFIRMED_WIDGET_SELECTOR { display: none !important; }',
});
await page.screenshot({ path: 'page.png' });
CSS injection is easy to review and can hide multiple known selectors in one rule. An inline style set through page.evaluate() is useful when you have already located a specific element. Neither method guarantees the widget stays hidden if the site removes or reconstructs it before the screenshot.
3. Handle delayed widget insertion
Third-party scripts may add a widget after the document loads. Waiting for a confirmed selector is the simplest approach when the widget eventually appears:
await page.waitForSelector('YOUR_CONFIRMED_WIDGET_SELECTOR', {
visible: true,
timeout: 15000,
});
await page.evaluate((selector) => {
const widget = document.querySelector(selector);
if (widget) widget.style.setProperty('display', 'none', 'important');
}, 'YOUR_CONFIRMED_WIDGET_SELECTOR');
await page.screenshot({ path: 'page.png' });
If the widget is repeatedly inserted or its styles are reset, a mutation observer can reapply the hiding rule. This is page-side logic, and it still depends on a correct selector and the widget being in the observed document:
await page.evaluate((selector) => {
const hide = () => {
const widget = document.querySelector(selector);
if (widget) widget.style.setProperty('display', 'none', 'important');
};
hide();
const observer = new MutationObserver(hide);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
}, 'YOUR_CONFIRMED_WIDGET_SELECTOR');
await page.waitForTimeout(500);
await page.screenshot({ path: 'page.png' });
The delay is an example, not a guarantee that a widget or page has finished rendering. Choose a wait condition that matches the site and check immediately before capture that the widget remains hidden.
4. Choose the screenshot extent
Hiding the widget and choosing what part of the page to capture are separate tasks. Puppeteer screenshot options control capture extent; they do not find or hide chat widgets.
// Viewport screenshot
await page.screenshot({ path: 'viewport.png' });
// Entire page
await page.screenshot({ path: 'full-page.png', fullPage: true });
// A specific rectangle in the page
await page.screenshot({
path: 'region.png',
clip: { x: 0, y: 0, width: 1200, height: 700 },
});
Use fullPage when the output should include the whole document, and clip when you need a known rectangle. These options do not change selector matching or widget timing. For other supported screenshot options, consult the official ScreenshotOptions API reference.
5. Account for iframes and shadow roots
Widget inside an iframe
A selector evaluated in the main document cannot select elements inside an iframe’s separate document. Find the relevant frame, then evaluate the selector in that frame:
const frame = page.frames().find((candidate) =>
candidate.url().includes('FRAME_HOST_OR_PATH')
);
if (!frame) {
throw new Error('Widget frame was not found');
}
await frame.waitForSelector('YOUR_CONFIRMED_WIDGET_SELECTOR', {
visible: true,
timeout: 10000,
});
await frame.evaluate((selector) => {
const widget = document.querySelector(selector);
if (widget) widget.style.setProperty('display', 'none', 'important');
}, 'YOUR_CONFIRMED_WIDGET_SELECTOR');
Use a frame identification condition that matches the destination site. Cross-origin restrictions apply to page JavaScript access, but Puppeteer frame evaluation runs in the selected frame context. Nested frames require locating the appropriate child frame.
Widget inside a shadow root
document.querySelector() does not cross a shadow-root boundary. If the site uses an open shadow root, locate the host and query within its shadowRoot:
await page.evaluate(() => {
const host = document.querySelector('YOUR_CONFIRMED_SHADOW_HOST_SELECTOR');
const widget = host?.shadowRoot?.querySelector(
'YOUR_CONFIRMED_WIDGET_SELECTOR_INSIDE_SHADOW_ROOT'
);
if (widget) widget.style.setProperty('display', 'none', 'important');
});
This approach requires an open shadow root and confirmed host and inner selectors. A closed shadow root is not available through this page-side query. In that case, look for a supported way to prevent the widget from loading or capture an area that excludes it.
6. Compare the hiding approaches
| Approach | Useful when | Watch for |
|---|---|---|
| Injected CSS | The selector is stable and the rule should be easy to inspect. | The widget may be inserted later, use another document, or recreate styles. |
| Inline style via evaluation | You want to find one element and set its display directly. | The selector must match the right element at evaluation time. |
| Mutation-aware logic | The widget appears late or is repeatedly reconstructed. | Observer scope and selector still matter; stop or disconnect observers if the page continues running. |
Puppeteer’s ElementHandle.screenshot() captures a selected element. It is not a way to hide an element from a page screenshot; the screenshots guide notes that this method may scroll an element into view if it is hidden. For hiding a widget in a page capture, apply the page-side change and use page.screenshot().
7. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
waitForSelector times out |
The selector is wrong, the widget does not load, it is inside a frame or shadow root, or it never becomes visible. | Inspect the page, verify the selector and context, and choose a timeout that fits the site’s load behavior. |
| The widget remains visible | The rule ran before insertion, matched a wrapper that does not contain the visible UI, or the widget reset its styles. | Wait for the actual visible element, target it precisely, and check just before capture. Use mutation-aware logic if the widget is recreated. |
| Other page content disappears | The selector is too broad. | Narrow it to a widget-specific element and verify the match before hiding it. |
| Evaluation returns no element | The element is absent from the main document or was added after evaluation. | Wait for it, then evaluate again in the correct frame or open shadow root. |
| Screenshot is blank or incomplete | The page may not have reached the state needed for capture, navigation may have failed, or the chosen clip is outside the expected content. | Check navigation errors, wait for a meaningful page condition, and confirm viewport or clip dimensions. |
| Browser process stays open after an error | Cleanup did not run on an exception. | Use try/finally around browser work and close the browser in the finally block. |
8. Performance, reliability, and cost
For a single capture, a targeted selector and one page-side evaluation add little work compared with launching a browser and loading the site. Reusing a browser across captures avoids repeated launches, but isolate pages and state as appropriate for your workload. Full-page screenshots can require more rendering and memory than viewport captures, especially on long pages.
Reliability depends on the target site: widget selectors can change, insertion timing varies, and frames or shadow roots require context-specific handling. Keep selectors configurable, fail clearly when a required widget is not found, and inspect a sample output when the target site changes. A wait timeout is a bound, not proof that the page is ready.
Self-hosted Puppeteer has no per-screenshot API charge from Puppeteer itself, but operating the browser environment consumes compute and maintenance time. The cost depends on your infrastructure, concurrency, and capture frequency; no fixed benchmark applies to every workload.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie and consent banners like a visitor, then removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. AI agents can use its MCP tools, including take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.
Install the Python dependency with pip install requests. Replace the example target URL and API key:
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()
open("shot.webp", "wb").write(r.content)
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Node.js:
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()));
See the ScreenshotNeo API documentation for request options. Sign up for 1,000 free screenshots a month with no card.
FAQ
Does Puppeteer have a built-in option to remove chat widgets?
No. Identify the widget in the target page and change the page before taking the screenshot.
Should I use ElementHandle.screenshot()?
Only when you want an image of that element. To hide a widget from a page capture, hide it first and call page.screenshot().
Will one selector work across every website?
No. Widget markup and rendering context vary by site, so confirm the selector and whether the widget is in the main document, an iframe, or a shadow root.


