How to Remove Cookie Banners from Playwright Screenshots
Remove cookie banners from Playwright screenshots by clicking consent, reusing storage state, or hiding the banner only during capture.

Direct answer: A cookie banner is usually ordinary page DOM. In Playwright, locate its consent button, click it, wait until the banner is hidden or detached, and then call page.screenshot(). For repeated runs, save the browser context’s storage state after consent and load it before later captures. If you only need a clean image and do not want to change browser state, use the screenshot style option to hide the banner and its backdrop for that capture.
This guide covers the three approaches, iframe and Shadow DOM edge cases, JavaScript dialogs, visual regression screenshots, complete TypeScript examples, troubleshooting, performance, reliability, and an API alternative when you do not want to maintain browser automation.
1. Choose the right removal method
| Method | Best for | What it changes | Reliability |
|---|---|---|---|
| Click consent | Production-like flows and compliance-sensitive captures | Cookies, localStorage, and page state | Highest behavioral fidelity |
Reuse storageState |
Large suites that repeatedly capture the same origins | Starts each context with saved consent state | High when consent storage is stable |
Screenshot style |
One-off images or visual baselines | Only the captured rendering | High if selectors are specific |
| Mask | Visual comparisons where pixels must be excluded | Adds an overlay over matched elements | Useful for assertions, not a natural clean image |
Use the real consent flow when the screenshot should represent a visitor who approved cookies. Use CSS hiding when the banner is irrelevant to the image and you deliberately do not want to persist consent. Do not assume cookie names, selectors, or storage behavior are portable between sites.
2. Click the consent control and wait for the banner to disappear
The most maintainable first attempt uses accessible roles, an accessible name, or a site-provided test ID. Avoid a broad text selector such as getByText('Accept') if the page can contain several unrelated buttons.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const banner = page.getByRole('dialog', { name: /cookie|consent/i });
const accept = banner.getByRole('button', {
name: /accept|agree|allow all/i,
});
if (await banner.isVisible().catch(() => false)) {
await accept.click();
await banner.waitFor({ state: 'hidden' });
}
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
waitFor({ state: 'hidden' }) handles both an element that remains in the DOM but becomes invisible and one that is detached. If the site animates the banner, wait for the final state rather than relying on a fixed sleep. A fixed delay can be too short on a slow run and waste time on a fast one.
When the page uses a different element
Not every consent component is a dialog. Adapt the locator to the site’s markup:
const banner = page.locator('#onetrust-banner-sdk');
const accept = banner.locator('#onetrust-accept-btn-handler');
if (await banner.isVisible().catch(() => false)) {
await accept.click();
await banner.waitFor({ state: 'hidden' });
}
Prefer stable attributes such as data-testid, an accessible name, or a documented component ID. Keep the locator scoped to the banner so a second “Accept” button elsewhere cannot be clicked accidentally.
Consent banners inside an iframe
If inspection shows the banner is inside an iframe, locate the frame and repeat the same sequence there. The frame may be cross-origin, but Playwright can interact with its DOM through a FrameLocator when the content is available.
const consentFrame = page.frameLocator('iframe[title*="consent" i]');
const frameBanner = consentFrame.getByRole('dialog');
const frameAccept = frameBanner.getByRole('button', {
name: /accept|agree|allow all/i,
});
if (await frameBanner.isVisible().catch(() => false)) {
await frameAccept.click();
await frameBanner.waitFor({ state: 'hidden' });
}
Some consent managers remove the iframe after acceptance. Waiting for the banner to become hidden is safer than waiting for a particular frame element to remain attached.
3. Save consent with Playwright storage state
After a successful click, save the context’s storage state. Playwright stores cookies and origin localStorage in the file, and a later browser context can load it before navigation. The state format is interchangeable between BrowserContext and APIRequestContext according to the Playwright API testing guide (official documentation).

One-time consent setup
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: /accept|agree|allow all/i }).click();
await context.storageState({ path: 'playwright/.auth/consent.json' });
await browser.close();
Reuse the saved state
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({
storageState: 'playwright/.auth/consent.json',
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
Keep authentication and consent files out of source control when they contain identifying cookies. Regenerate the file when the site changes its consent provider, cookie domain, or localStorage keys. A state file created for www.example.com may not apply to a different subdomain.
4. Hide the banner only while taking the screenshot
Playwright’s screenshot style option injects a stylesheet for the capture. It is designed for hiding dynamic elements and can pierce Shadow DOM and apply to inner frames (Page API documentation). The page is not permanently modified.

import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({
path: 'page.png',
fullPage: true,
style: `
[role="dialog"][aria-label*="cookie" i],
.cookie-banner,
.cookie-consent,
.consent-backdrop {
display: none !important;
}
`,
});
await browser.close();
Include the backdrop as well as the panel. Otherwise a fixed overlay can still dim the page or block visible content. Scope selectors to the current site’s known classes or attributes. A generic rule such as div[role="dialog"] can hide a legitimate login, promotion, or accessibility dialog.
External CSS with stylePath
For visual regression suites, keep the hiding rules in a fixture file:
/* tests/fixtures/hide-consent.css */
#onetrust-banner-sdk,
#onetrust-consent-sdk,
.consent-backdrop {
display: none !important;
}
await expect(page).toHaveScreenshot('homepage.png', {
stylePath: 'tests/fixtures/hide-consent.css',
});
Playwright documents stylePath for screenshot assertions. Use it when the intended baseline should contain no banner. Use mask when the element may remain but its pixels should be covered:
await expect(page).toHaveScreenshot('homepage.png', {
mask: [page.locator('.cookie-banner')],
});
A mask is an overlay, so it does not create a natural page image. CSS hiding or real consent is preferable when the output is published or sent to a customer.
5. JavaScript dialogs are a separate problem
page.on('dialog') handles browser JavaScript dialogs created by alert, confirm, prompt, or beforeunload. It does not select an ordinary cookie banner rendered as a div or custom dialog component. Playwright automatically dismisses dialogs when no listener is installed; when you do install a listener, the handler must accept or dismiss the dialog or the triggering action can stall (Dialogs documentation).
page.on('dialog', async dialog => {
await dialog.dismiss();
});
Install this handler only when the site actually raises a JavaScript dialog. Continue using locators for DOM-based consent.
6. A robust capture helper
For a test suite covering multiple sites, isolate consent handling and make failure behavior explicit. This helper first tries a supplied locator, then captures the page only after the banner is gone.
import type { Page, Locator } from 'playwright';
export async function removeConsentAndCapture(
page: Page,
banner: Locator,
accept: Locator,
path: string,
) {
if (await banner.isVisible().catch(() => false)) {
await accept.click();
await banner.waitFor({ state: 'hidden' });
}
await page.screenshot({ path, fullPage: true });
}
Pass site-specific locators from each test. This keeps the timing and screenshot behavior consistent without pretending that every consent vendor has the same markup.
7. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Locator does not resolve | The banner is not a role="dialog", or text differs by locale |
Inspect the DOM; use a stable test ID, vendor ID, accessible name, or language-aware matcher. |
| Click times out | An overlay intercepts the click, the button is outside the current frame, or the banner has not finished rendering | Use the correct frame locator, wait for visibility, and inspect the actionability error before considering a forced click. |
| Banner returns on every run | Consent was not saved, storage state was loaded after navigation, or the cookie belongs to another domain | Save state after the click and pass storageState when creating the context, before opening the page. |
| CSS hiding leaves a gray page | The backdrop is a separate element | Hide the backdrop selector as well as the panel. |
| Banner is inside an iframe | Page-level locators cannot see frame DOM | Use page.frameLocator() and perform the click inside that frame. |
| Screenshot captures the banner during its animation | Capture occurs immediately after clicking | Wait for hidden or a site-specific detached state; avoid arbitrary sleeps where possible. |
| Visual baseline is unstable | Dynamic consent, ads, or timestamps still render | Use stylePath for known dynamic elements and mask only pixels that should be ignored. |
page.on('dialog') does nothing |
The consent UI is DOM, not a JavaScript dialog | Use a locator or screenshot CSS. |
8. Performance, reliability, and cost considerations
- Startup: Reusing a browser and contexts is faster than launching a new browser for every URL. A saved storage state also removes the need to repeat the consent flow.
- Waiting: Prefer locator state and meaningful navigation conditions.
networkidlecan be unsuitable for pages with analytics or long-lived connections; use a targeted selector or a short, justified delay when necessary. - Parallelism: Use separate contexts for concurrent captures so cookies and localStorage do not leak between tests. Limit concurrency according to available CPU and memory.
- Reliability: Record whether consent was clicked, whether the banner disappeared, and which URL and frame were used. A screenshot can be visually clean while the underlying page still failed to load.
- State safety: Storage files contain cookies and localStorage. Protect them and use a dedicated test account or consent-only state where appropriate.
- Cost: Self-hosted Playwright costs the compute time of your browser workers and any proxy or CI infrastructure you add. A screenshot API trades browser maintenance for per-capture pricing; compare the number of URLs, retry rate, and need for custom interaction.
9. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes the features, with 1,000 screenshots per month free without a card and paid plans starting at $5 for 3,000 shots.
See the ScreenshotNeo API documentation for authentication and options. A one-call capture looks like this:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
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 supports full-page and element captures, device presets or custom viewports, dark mode, retina scale, custom CSS and JavaScript, clicks, waits, blocked resource types, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching with a chosen TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, PDFs, and HTML/CSS rendering. These options let you replace much of the Playwright setup while keeping the request model simple.
Create a free ScreenshotNeo account for 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots.
10. Practical checklist
- Identify whether the consent UI is DOM, an iframe, or a JavaScript dialog.
- Prefer an accessible role, label, or stable test ID over broad text matching.
- Click consent and wait for hidden or detached state before capture.
- Save and reuse
storageStatefor repeated runs. - Use screenshot
styleorstylePathwhen hiding should affect only the image. - Hide both the banner and its backdrop.
- Use
maskonly when an overlay is acceptable in a visual assertion. - Keep selectors site-specific and review them after redesigns.
- Protect storage files containing cookies.
- Log verdicts, URLs, frames, and timing when diagnosing flaky captures.
FAQ
Does hiding a banner with CSS set consent cookies?
No. Screenshot CSS changes the captured rendering only. If later navigation depends on consent, click the control and save storage state instead.
Can I use one storage-state file for every website?
No. Cookies and localStorage are scoped by origin and domain. Create state for the origins your tests actually visit.
Should I force-click the accept button?
Usually no. A forced click can hide an overlay or timing problem and may not represent a real visitor action. Fix the frame, locator, or readiness condition first.
Why does a banner appear only in headless mode?
Sites can vary by viewport, user agent, geolocation, or timing. Record those settings and inspect the rendered DOM in the same context used for the screenshot.
Is a mask equivalent to removing the banner?
No. A mask covers pixels in the assertion image. It does not remove the element, its backdrop, or its effects on layout.


