ScreenshotNeo

BlogHow-to

Puppeteer Screenshot with CSS Injected Before Capture

Inject CSS after navigation and page readiness, then capture with Puppeteer. See runnable examples for full-page, clipped, and element screenshots.

By the ScreenshotNeo team4 October 20268 min read

To apply CSS before a Puppeteer screenshot, navigate to the page, wait until the content you need is ready, await page.addStyleTag({content: css}), then call page.screenshot(). Awaiting the style injection matters: it ensures Puppeteer has inserted the style element before capture. For an entire page, set fullPage: true; for a single element, use ElementHandle.screenshot(). See the official Page.addStyleTag API and Puppeteer screenshots guide.

1. Install Puppeteer and take a screenshot with injected CSS

This runnable Node.js example saves a full-page PNG. It hides a cookie banner and applies a page-level background override. Replace the example URL and selectors with those for the page you capture.

npm init -y
npm install puppeteer

Save as screenshot.mjs and run node screenshot.mjs:

import puppeteer from 'puppeteer';

const url = 'https://example.com';
const css = `
  html { background: #fff !important; }
  body { background: #fff !important; }
  .cookie-banner { display: none !important; }
`;

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });

  await page.goto(url, { waitUntil: 'networkidle2' });
  await page.addStyleTag({ content: css });
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
  await browser.close();
}

The order is intentional: navigation and readiness → CSS injection → screenshot. addStyleTag() returns a promise for the inserted style element, so await it before capture. The example uses networkidle2, which appears in Puppeteer’s screenshots guide; it is a starting point, not a universal readiness rule. Some sites keep connections open or render important content after the network quiets down.

2. Choose a readiness condition that matches the page

Waiting for navigation to finish is not always the same as waiting for the specific content your screenshot needs. Select a navigation wait condition based on how the site works, then add a selector or application-specific check if the target content appears later.

  • Use waitUntil: 'load' when the page’s load event is a useful boundary for your capture.
  • Use waitUntil: 'domcontentloaded' when you only need the initial document parsed and will wait explicitly for the content afterward.
  • Use waitUntil: 'networkidle2' when a quiet network is a reasonable signal for that site. Analytics, polling, streaming, and long-lived requests may make it unsuitable.
  • Wait for a selector when a particular element must exist. For example, after navigation, call await page.waitForSelector('[data-report-ready]').
  • Wait for application state when the element can exist before its content is complete. A site-specific DOM check or known readiness marker can be more reliable than a fixed delay.

Example with an explicit readiness selector:

await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready]', { timeout: 15000 });
await page.addStyleTag({ content: '.cookie-banner { display: none !important; }' });
await page.screenshot({ path: 'report.png', fullPage: true });

If the selector never appears, the wait rejects with a timeout instead of silently capturing an incomplete page. Choose a selector that signals the actual content is ready, not merely that the page shell exists.

3. Write CSS that affects the intended content

Pass inline CSS in the content property. Use normal CSS selectors, and add !important when site rules with greater specificity or later injection might override your declaration.

const css = `
  .newsletter-modal,
  .chat-widget,
  .cookie-banner {
    display: none !important;
  }

  .report { max-width: 1100px !important; margin-inline: auto !important; }
  body { color: #222 !important; background: #fff !important; }
`;
await page.addStyleTag({ content: css });

CSS injection changes the rendered page; it does not delete the corresponding DOM nodes or alter the site’s server-side content. For content inside an iframe, inject into that frame rather than assuming a stylesheet added to the main page will cross the frame boundary.

Hide an element by selector

For a stable, unique selector, display: none is the direct approach. If the page creates the element later, inject after that content is ready or use a selector that will match when the element appears. If a site replaces the element after injection, re-check the capture timing.

await page.addStyleTag({
  content: '#consent-dialog, .floating-chat { display: none !important; }',
});

Use an external stylesheet

page.addStyleTag() can also add a stylesheet by URL. This is useful when the stylesheet is already hosted and reachable by the browser. The returned promise resolves to the inserted link element; wait for it before capturing. An inline content string avoids depending on an external stylesheet request.

await page.addStyleTag({ url: 'https://example.com/screenshot-overrides.css' });

4. Pick the capture scope and output options

page.screenshot(options) captures the page. The official ScreenshotOptions reference documents the following commonly used settings; confirm details against the Puppeteer version installed in your project.

Option What it does When to use it
path Saves the screenshot to a file. The extension can determine the image type. When the result should be written to disk, such as path: 'page.png'.
type Selects the image format; PNG is the documented default. When you need to specify a supported format explicitly.
fullPage Captures the full page; default is false. When content below the current viewport must be included.
clip Captures a rectangular region of the page. When you need a specific viewport-coordinate rectangle.
captureBeyondViewport Controls capture beyond the viewport; its documented default depends on whether clip is set. When using a clip or controlling capture outside the viewport.
omitBackground Omits the default white background for transparency where supported. When the output needs a transparent background.
quality Sets image quality from 0 to 100 for applicable formats; it does not apply to PNG. When using a lossy image type and balancing size against visual quality.
encoding Controls returned data encoding, including binary or base64. When consuming the screenshot in memory rather than saving by path.

For a clipped capture, specify coordinates and dimensions for the region you want:

await page.screenshot({
  path: 'header.png',
  clip: { x: 0, y: 0, width: 1440, height: 220 },
});

Set the viewport before navigation when the page’s responsive layout depends on viewport width or device scale. Full-page capture changes the capture scope; it does not guarantee that every lazy-loaded image or infinite-scroll section has been rendered. Scroll or trigger the site’s own loading behavior and wait for the required content before capture.

5. Capture one element instead of the whole page

Use ElementHandle.screenshot() when the output should contain a single element. Puppeteer’s guide notes that element screenshots try to scroll the element into view. The method throws if the element has been detached from the DOM, so locate it after navigation and after any page update that might replace it.

await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.addStyleTag({ content: '.cookie-banner { display: none !important; }' });

const card = await page.waitForSelector('.product-card', { timeout: 10000 });
if (!card) throw new Error('Product card was not found');
await card.screenshot({ path: 'product-card.png' });

Use a page screenshot with clip if you need a fixed rectangular region rather than the bounds of a DOM element. Use an element screenshot if the element itself defines the desired capture boundary.

6. Common problems and fixes

Symptom Likely cause Fix
The screenshot still shows the element. The selector does not match, the CSS loses to another rule, the element is in a child frame, or the page replaced it after injection. Check the selector against the rendered DOM; add specificity or !important; inject into the relevant frame; inject after the content is created.
The screenshot is taken before content appears. Navigation completed before client-rendered or delayed content was ready. Wait for a content-specific selector or application readiness check before injecting and capturing.
networkidle2 never resolves or takes too long. The page may keep requests open or continue polling. Use a different navigation wait and wait for the specific content needed. Set a timeout appropriate to your job.
waitForSelector times out. The selector is wrong, conditional, or not reached due to navigation, authentication, or a page error. Confirm the URL and selector, handle required authentication, and inspect whether the content is present in the target frame.
Element screenshot reports a detached element. The page replaced or removed the element handle after it was obtained. Wait for the page update to finish, then query the element again immediately before capture.
CSS is missing from an iframe. page.addStyleTag() is a shortcut for the main frame and does not automatically target child frames. Find the correct frame and call its addStyleTag() method.
Output is blank, truncated, or has the wrong layout. The page was not ready, viewport dimensions were unexpected, or the capture scope does not match the desired result. Set the viewport deliberately, wait for page-specific readiness, and choose full-page, clip, or element capture as appropriate.
Image file is much larger than expected. PNG preserves lossless image data and may be large for detailed or tall pages. Use a supported lossy image type and a suitable quality value if the use case permits; quality does not apply to PNG.

7. Reliability, performance, and cost considerations

  • Keep browser cleanup in finally. Closing the browser after success or failure prevents abandoned browser processes in a long-running script.
  • Bound waits. Use explicit selector timeouts and an application-appropriate navigation strategy so a slow or permanently active page does not stall a capture job indefinitely.
  • Use the narrowest useful capture. A full-page screenshot can include much more content than a viewport or element capture. Prefer the smallest scope that satisfies the output requirement.
  • Reuse browser processes for batches. When capturing many pages in one process, reuse the browser and create/close pages deliberately instead of launching a new browser for each capture. Isolate page state when captures need separate cookies or sessions.
  • Account for site behavior. Authentication, consent state, responsive layout, animation, lazy loading, and frame boundaries can all change what appears. CSS injection does not make unavailable content available.
  • Budget infrastructure, not just capture time. A self-hosted Puppeteer workflow uses compute and browser memory as well as network and storage. Measure your own workload and set concurrency based on available resources; no single throughput figure applies to every target site.

8. Or skip the browser setup

If you need a screenshot API instead of managing a browser, ScreenshotNeo takes a screenshot from one GET request. See the ScreenshotNeo API docs. Example request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

9. FAQ

Does injected CSS change the original website?

It changes the page rendered in the Puppeteer browser session. It does not publish changes to the website or modify its source files.

Can I inject CSS before the page loads?

page.addStyleTag() operates on the current page frame. The straightforward workflow is to navigate, wait for the necessary page state, inject CSS, then capture. If styles must apply from the earliest rendering stage, use a browser setup designed to add them before page scripts and styles run.

Can the CSS hide content from a cross-origin iframe?

Not by injecting into the main frame. Target the iframe’s Puppeteer frame and inject the stylesheet there, if you can access that frame.

Is networkidle2 always the best wait condition?

No. It is used in Puppeteer’s screenshot guide example, but a page-specific selector or readiness signal is often more appropriate when network activity does not represent visual readiness.

References