Fix Puppeteer Screenshots That Show a Website’s Cookie Banner Over the Page
Choose whether to test real consent or capture the page layout. Then use isolated browser state and a site-specific fix to get a repeatable Puppeteer screenshot.
If a Puppeteer screenshot shows a cookie banner over the page, first decide what the screenshot is meant to prove. For a real visitor or consent test, interact with the banner and wait for it to close. For a visual-only layout capture, use a controlled browser context and a narrowly targeted, site-specific CSS override immediately before capture. Hiding a banner changes the image; it does not grant consent.
Puppeteer captures the rendered page, so a banner that is still visible will appear in page.screenshot(). There is no universal Puppeteer option or selector that removes every site’s cookie banner. The right selector and interaction depend on the website and consent manager. See the Puppeteer Page screenshot API and Browser management guide.
1. Choose the test you are running
| Goal | Correct approach | What the screenshot means |
|---|---|---|
| Test a real visitor journey or consent behavior | Locate the banner and explicitly click the intended accept, reject, or settings action; wait for the resulting state. | The capture follows the consent action taken in that test. |
| Compare the page layout without consent UI | Use a fresh context, then apply a narrowly scoped temporary style to the verified banner just before capture. | The banner is visually suppressed for the capture. No consent choice was made. |
| Reproduce a returning visitor | Use a controlled persisted browser context with the intended consent state. | The capture reflects that saved browser state. |
Keep consent-flow tests and visual-cleanup captures separate. Do not describe a hidden banner as accepted consent.
2. Make browser state repeatable
Cookies and local storage can persist a consent decision, so the banner may appear in one run and disappear in another. A new Puppeteer BrowserContext isolates cookies and local storage and provides a clean starting point. A clean context may make the banner appear again; that is expected.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
const page = await context.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Inspect the page and perform the consent or visual-only step below.
} finally {
await context.close();
await browser.close();
}
For a repeat-visitor test, deliberately reuse the appropriate persisted state instead of creating a fresh context each time. Avoid letting one test silently inherit another test’s consent choice.
3. Find the actual banner before changing it
Do not guess a selector such as .cookie-banner and assume it is universal. Inspect the target page in DevTools or with Puppeteer to determine whether the consent UI is a normal DOM element, inside an iframe, or inside a shadow root. Identify the specific control and expected outcome when testing consent.
A quick DOM inspection can help locate likely candidates:
const candidates = await page.evaluate(() =>
[...document.querySelectorAll('body *')]
.filter(el => /cookie|consent|privacy/i.test(
`${el.id} ${el.className} ${el.getAttribute('aria-label') || ''}`
))
.slice(0, 30)
.map(el => ({ tag: el.tagName, id: el.id, className: String(el.className) }))
);
console.log(candidates);
This is only a search aid. It can miss third-party markup, frames and shadow DOM, and it can return unrelated elements. Verify the result visually and in the page structure before acting.
4. For a real consent flow, click the intended choice
Use a selector confirmed on the site, click the exact choice the test is intended to exercise, and wait for the banner to disappear or for a known post-consent state. The following is a runnable pattern; replace the URL and selector with values inspected for your target.
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const acceptSelector = '[data-testid="consent-accept"]'; // replace after inspection
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector(acceptSelector, { visible: true, timeout: 10000 });
await page.click(acceptSelector);
await page.waitForSelector(acceptSelector, { hidden: true, timeout: 10000 });
await page.screenshot({ path: 'consented.png', fullPage: true });
} finally {
await context.close();
await browser.close();
}
Use the reject or settings control instead when that is the behavior under test. If the banner closes asynchronously, wait for a verified state change rather than adding an arbitrary long sleep. If the banner is inside an iframe, locate the relevant frame and interact with its frame context; a selector on the top-level page will not reach into it.
5. For visual-only cleanup, hide only the verified element
When the task is a page-layout image and no consent behavior is being tested, inject a temporary style for the specific, inspected element. The selector below is deliberately a placeholder:
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const bannerSelector = '#verified-consent-root'; // inspect and replace
const browser = await puppeteer.launch();
const context = await browser.createBrowserContext();
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector(bannerSelector, { visible: true, timeout: 10000 });
await page.addStyleTag({
content: `${bannerSelector} { display: none !important; }`,
});
await page.screenshot({ path: 'layout.png', fullPage: true });
} finally {
await context.close();
await browser.close();
}
addStyleTag injects a style into the page; the selector choice and visual-only use are your implementation, not a built-in cookie-banner feature. Prefer this reversible style to deleting broad page content. Check that it hides only the consent UI and does not cover or remove a layout region you meant to capture. A site may render its banner in a shadow root or iframe, where this top-level style does not apply; handle that structure specifically.
6. Capture at the right time
Choose the navigation wait condition for the site and capture goal. domcontentloaded can return before client-rendered consent UI appears; networkidle can be unsuitable on pages with continuous network activity. In either case, wait for the specific banner or page state you need, then capture. Avoid relying on a fixed delay unless the target has a known timing requirement.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector(bannerSelector, { visible: true, timeout: 10000 });
// Perform the consent action or visual-only override here.
await page.screenshot({ path: 'screenshot.png', fullPage: true });
Set fullPage: true only when you need the full scrollable page; otherwise the screenshot captures the current viewport. Puppeteer screenshots capture rendered page content, including any banner still present.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Banner appears inconsistently across runs | A prior run saved consent in cookies or local storage. | Use a fresh BrowserContext for clean-state runs, or deliberately reuse a controlled persisted context for returning-visitor tests. |
waitForSelector times out |
The selector is wrong, the banner has not rendered, or it lives in a frame or shadow root. | Inspect the page structure, wait for the correct state, and query the correct frame or shadow root. |
| CSS injection succeeds but banner remains | The selector does not identify the visible container, or the banner is isolated in a frame or shadow root. | Inspect the rendered element and target the actual container in its own DOM context. |
| Screenshot is taken before the banner appears or closes | Navigation readiness was mistaken for consent UI readiness, or the close transition is asynchronous. | Wait for the banner to become visible, then wait for its verified hidden state after the action. |
| Clean context makes the banner return | No consent state exists in that isolated context. | This is expected. Make a deliberate consent choice for a consent-flow test or apply visual-only cleanup for a layout image. |
| Page-level cookie API warning | Puppeteer documents page-level cookie methods as deprecated. | Use the Browser or BrowserContext cookie APIs described in the current documentation. |
8. Headless mode and extensions
Puppeteer runs headless by default. Do not assume an extension-based banner solution will work in headless automation: one documented setup in the research required headful mode, but that does not establish a general limitation for every current extension. A site-specific DOM or consent interaction is easier to reason about for a controlled test. See the Puppeteer project overview.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its API takes a URL in one GET request and returns a PNG, JPEG, WebP or PDF. For example, this cURL request saves a WebP capture:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
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 Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
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 cleanup step can be turned off. Bot checks, blank pages and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info and capture_pdf for AI agents. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. If you need to verify consent behavior, keep using a real consent-flow test rather than treating visual cleanup as consent. Sign up for 1,000 free screenshots a month, no card required.
Performance, reliability and cost
- Keep runs focused: a new browser context makes state deterministic, but it does not eliminate page loading or rendering work. Capture only the viewport or full page you need.
- Wait on conditions: a selector or meaningful page state is usually more reliable than a long fixed sleep. Set navigation and selector timeouts so failures are visible rather than hanging indefinitely.
- Keep cleanup narrow: broad CSS can hide content and create misleading visual comparisons. Store the site-specific selector and the test intent alongside the capture job.
- Cost: Puppeteer is a browser automation library; the supplied code has no screenshot API per-shot pricing claim. Account for the machine and browser resources used by your own automation. ScreenshotNeo’s stated free and paid plan limits are listed above.
FAQ
Does Puppeteer have a built-in option to remove all cookie banners?
No universal removal option is documented. Identify the site’s consent implementation and choose a real consent interaction or a visual-only, site-specific change.
Why does the banner return after I hide it?
A visual style changes the current rendered page; it does not save a consent choice. A later page load or fresh context can render the banner again.
Should I accept cookies to get a clean screenshot?
Only if acceptance is the action your test is meant to make. For a layout capture, hide the verified banner as a visual-only step and label the result accordingly.
Can I reuse my browser cookies?
Yes, for a returning-visitor scenario when that saved state is intentional and controlled. Use an isolated context when you need a clean, repeatable first-visit state.


