How to Remove Page Elements Before Taking a Screenshot in Node.js
Remove banners, popups, ads, and other elements before capturing a page in Node.js with Playwright or Puppeteer. Compare DOM removal with screenshot-only CSS.

To remove page elements before taking a screenshot in Node.js, run JavaScript in the browser page to select and remove the unwanted DOM nodes, then call the screenshot API. With Playwright, use page.evaluate() or a locator’s evaluateAll(); with Puppeteer, use page.evaluate() or page.$$eval(). If you only want the screenshot to look clean and do not need to mutate the page, Playwright’s screenshot style option can hide elements with CSS during capture.
This guide covers both approaches, timing, iframes and dynamic pages, complete Playwright and Puppeteer examples, troubleshooting, and capture performance. Choose specific selectors and inspect what they match: a broad selector can remove content you meant to keep.
1. Choose between removing and hiding elements
First decide whether you need to change the page’s document tree or only its appearance in the image.
| Approach | What changes | Use it when |
|---|---|---|
| Remove DOM nodes | The selected elements are detached from the document. | You need a capture that reflects their removal, or want to perform other page-context work before taking the image. |
| Hide with screenshot CSS | The browser applies styles for the capture; the page DOM remains intact. | The change is only for the screenshot and your capture API supports injected screenshot styles. |
| Mask matched elements | The screenshot covers matched content, but does not delete it. | You need to obscure sensitive or distracting content rather than remove its layout and nodes. |
| Capture one element | The output is an image of a targeted component. | You only need a chart, card, or other component, not the full page. |
Playwright documents screenshot styles for changing appearance and hiding dynamic elements; the injected stylesheet also reaches Shadow DOM and inner frames. Puppeteer documents page evaluation and screenshot capture, but the screenshot-style option shown below is specific to Playwright. For either library, check the documentation version matching your installed package.
2. Remove elements with Playwright
Install Playwright and its browser if they are not already available in your project:

npm install playwright
npx playwright install chromium
Save this as capture.mjs and run it with node capture.mjs. It opens a page, waits for the page load event, removes every current match for each selector, and writes a full-page PNG.
import { chromium } from 'playwright';
const url = 'https://example.com';
const selectors = ['.cookie-banner', '#newsletter-modal', '.sticky-ad'];
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
await page.evaluate((selectors) => {
for (const selector of selectors) {
document.querySelectorAll(selector).forEach((element) => element.remove());
}
}, selectors);
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
page.evaluate() executes the function in the browser page context, where document and the DOM are available. Its argument is serialized into that context; keep the callback self-contained and pass data such as selectors explicitly. The try/finally ensures the browser is closed even when navigation, cleanup, or screenshot capture throws.
Remove all current matches with a locator
For one combined selector, a locator is concise:
await page.locator('.cookie-banner, #newsletter-modal, .sticky-ad')
.evaluateAll((elements) => elements.forEach((element) => element.remove()));
await page.screenshot({ path: 'page.png' });
evaluateAll() runs on the elements matched at that moment. It does not keep watching for new matches. If a client-side script inserts a banner afterward, wait for the relevant state and clean up immediately before capture, or use a different strategy for the page.
3. Hide elements only for a Playwright screenshot
When no other page interaction depends on deleting the nodes, inject CSS through the screenshot call:

await page.screenshot({
path: 'page.png',
fullPage: true,
style: `
.cookie-banner,
#newsletter-modal,
.sticky-ad {
display: none !important;
}
`,
});
This keeps the document tree intact while hiding the matching elements for the screenshot. It can be a good fit for pages where elements are injected dynamically, since the styles are applied for capture. Use selectors that identify only the unwanted UI. Hiding a large container can also hide useful content or alter the page layout.
Playwright also supports screenshot options such as disabling animations and masking matched locators. A mask places a cover over the matched area; it does not remove the corresponding nodes. Use it when obscuring is the goal and be aware that the covered area still occupies its layout space.
4. Remove elements with Puppeteer
Install Puppeteer, which downloads a compatible browser as part of its standard setup:
npm install puppeteer
This complete example uses page.evaluate() to remove a list of selectors, then saves the screenshot. Run it as node capture-puppeteer.mjs.
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const selectors = ['.cookie-banner', '#newsletter-modal', '.sticky-ad'];
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
await page.evaluate((selectors) => {
for (const selector of selectors) {
document.querySelectorAll(selector).forEach((element) => element.remove());
}
}, selectors);
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
If all unwanted nodes share one selector, Puppeteer’s $$eval() is a shorter alternative:
await page.$$eval('.cookie-banner, #newsletter-modal, .sticky-ad', (elements) => {
elements.forEach((element) => element.remove());
});
await page.screenshot({ path: 'page.png' });
Puppeteer also offers ElementHandle.screenshot() for a particular element. That can avoid cleaning a whole page when the output only needs one component. [Puppeteer screenshot guide](https://pptr.dev/guides/screenshots) and [page interactions guide](https://pptr.dev/guides/page-interactions) document the screenshot and evaluation APIs.
5. Timing, selectors, and page edge cases
Clean up after the unwanted element appears
Removing a node before the page’s scripts create it has no effect. Conversely, removing it early can be undone if a later script inserts it again. Navigate first, wait for a page-specific readiness signal, then clean up immediately before capture.
For a page where the banner is known to appear, wait for it before removal. A timeout can be expected if it is optional; decide how your workflow should treat that case.
const banner = page.locator('.cookie-banner');
try {
await banner.waitFor({ state: 'visible', timeout: 5_000 });
} catch {
// This page may not show the banner. Continue with the capture.
}
await page.locator('.cookie-banner').evaluateAll((elements) =>
elements.forEach((element) => element.remove())
);
await page.screenshot({ path: 'page.png', fullPage: true });
Do not treat a created locator as proof that content has settled. Playwright’s locator documentation describes locators as the central piece of its auto-waiting and retry-ability, but list retrieval such as locator.all() does not wait for matching elements and can be unpredictable while a list changes. Choose a signal that reflects the target site’s actual state.
Choose selectors narrowly
Prefer a stable ID or a class specific to the unwanted component over generic selectors such as div, aside, or [role=dialog]. Dialogs can contain content that matters. During development, inspect the count and relevant attributes before removing matches:
const count = await page.locator('.cookie-banner').count();
console.log(`Matched ${count} cookie banner element(s)`);
Counts are a useful sanity check, not a substitute for checking the result image. A page may use a different selector at another viewport or after a redesign.
Handle frames and shadow roots
A top-level document.querySelectorAll() does not traverse into iframe documents. With Playwright, target the frame’s document when removing nodes:
const frame = page.frame({ name: 'consent-frame' });
if (frame) {
await frame.evaluate(() => {
document.querySelectorAll('.cookie-banner')
.forEach((element) => element.remove());
});
}
Find frames using the page’s actual frame URL or name; those values are site-specific. For Shadow DOM, ordinary document selectors do not cross a shadow boundary. Playwright’s screenshot style option is documented to pierce Shadow DOM and inner frames for styling, which can make capture-only hiding simpler where it applies.
Account for full-page capture and layout
Removing a fixed overlay usually reveals what it covered. Removing an in-flow banner may also cause content below it to move up, which is a natural consequence of changing the layout. If preserving the space matters, hide the element with visibility: hidden rather than removing it or setting display: none.
A full-page screenshot can be much taller than the viewport and use substantially more memory than a viewport capture. Lazy-loaded images may not have loaded just because navigation completed. If the page relies on scrolling to load content, scroll deliberately and wait for the images or sections your output requires before the final capture. The exact readiness condition depends on the site.
6. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF, with options for full-page capture, CSS selectors, custom CSS and JavaScript, wait conditions, device viewports, and more. See the API documentation for request options.
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free 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.
7. cURL and Python alternatives
The same API can be called outside Node.js when useful for scripts or debugging. Use your own API key and encode the target URL as a query parameter.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
These calls illustrate an API capture rather than local DOM manipulation. For your own browser-controlled cleanup logic, use the Playwright or Puppeteer examples above. The API also supports custom CSS and JavaScript when you need to control page appearance or behavior as part of a capture.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The element remains in the image. | The selector matched nothing, the element appeared later, or it lives in a frame or shadow root. | Check the match count, wait for the page-specific state, repeat cleanup immediately before capture, and target the correct frame. Consider Playwright screenshot CSS for capture-only styling. |
| Page content disappeared too. | A selector matched a parent container or common elements beyond the target. | Use a more specific selector, inspect matched nodes, and compare a fresh capture before widening the cleanup. |
| The banner returns after cleanup. | A client-side script reinserted it. | Move cleanup closer to capture, wait until the page’s relevant updates finish, or use screenshot CSS in Playwright. |
| The screenshot is blank or incomplete. | Navigation completed before the app rendered, or the site needs a different readiness signal. | Wait for a meaningful selector or page state rather than relying on a fixed short delay. Inspect console errors and network failures if the problem persists. |
| Iframe content was not changed. | Top-level evaluation only accessed the main document. | Evaluate within the target frame. For Playwright capture styling, note that its screenshot style is documented to apply to inner frames. |
| Capture is slow or memory-heavy. | The page is long, resources are large, or the workflow waits for network idle on a page with ongoing requests. | Capture only the required viewport or element, avoid unnecessary resource waits, and choose a readiness condition suited to the page. |
| The browser stays open after an error. | Cleanup of the browser was skipped on an exception path. | Put browser shutdown in a finally block, as in the runnable examples. |
9. Performance, reliability, and cost
For a local Playwright or Puppeteer job, the largest costs are usually browser startup, navigation, and loading the page’s resources. Keep one browser process alive across a batch of captures where appropriate, create isolated pages for separate jobs, and close pages when finished. Avoid capturing the full page if the consumer only needs the viewport or a single element. Full-page output consumes more time and memory as page height and image dimensions grow.
Waiting for networkidle can be a poor fit for sites that keep analytics, chat, or streaming connections open. Prefer a page-specific selector or a clear application state, then perform the cleanup and capture. Fixed sleeps add latency when a page is ready early and may still be too short on a slow page.
Local browser automation has no per-capture API fee, but it uses your compute, browser installation, and maintenance time. A hosted screenshot API shifts browser operation to the service and charges according to its plan and billing rules. ScreenshotNeo’s published tiers are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Its response includes page-verdict and billing headers; only clean shots are billed, while bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Treat those as product-specific terms, not a general property of screenshot APIs.
10. FAQ
Does removing an element change the live website?
In these examples, JavaScript changes the DOM in the automated browser page. It does not edit the site’s server-side source files. The change is local to that browser session unless your own code separately submits data or writes changes elsewhere.
Can I remove an element by its text instead of a CSS selector?
You can locate elements by text with browser automation APIs, but text may be duplicated, localized, or changed by the site. For repeatable captures, prefer a selector tied to the component and verify the matches before removing anything.
Can I use this for a screenshot of a single chart?
Yes. If only the chart matters, capture its element directly with the library’s element screenshot capability. Removing unrelated page elements is unnecessary unless they overlap or affect the chart.
Which should I use: Playwright or Puppeteer?
Either supports page-context evaluation followed by a screenshot. Choose based on the browser automation library already used in your project and the APIs available in its installed version. The screenshot-only CSS technique described here is documented for Playwright.


