ScreenshotNeo

BlogHow-to

How to Add Custom CSS to a Page with Puppeteer

Use Puppeteer’s page.addStyleTag() to inject CSS into a loaded page, target a child frame, or load a stylesheet from a URL.

By the ScreenshotNeo team4 October 20266 min read

Use Puppeteer’s page.addStyleTag() to add CSS to a page that is already open. Pass a CSS string with content for inline rules, or a stylesheet URL with url. Await the call before taking a screenshot or doing other work that depends on the styles.

await page.addStyleTag({
  content: 'body { background: papayawhip; }',
});

The method inserts a <style> element or a <link rel="stylesheet"> into the main frame and returns a handle to that element. See the Puppeteer Page.addStyleTag() API reference.

1. Inject inline CSS into a loaded page

This complete Node.js example opens a page, injects CSS, waits for the page’s main heading, and saves a screenshot. Install Puppeteer with npm install puppeteer, save the code as capture.mjs, then run node capture.mjs.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  await page.addStyleTag({
    content: `
      body {
        background: #f4f1ea;
        color: #20242a;
        font-family: system-ui, sans-serif;
      }
      h1 {
        color: #175cd3;
      }
    `,
  });

  await page.waitForSelector('h1');
  await page.screenshot({ path: 'styled-page.png', fullPage: true });
} finally {
  await browser.close();
}

For CommonJS projects, replace the import with const puppeteer = require('puppeteer'); and run the script in a CommonJS file. The browser must be launched before creating a page, and the page must be navigated before styling an existing document.

2. Load CSS from a URL

When the stylesheet already exists at a reachable URL, pass url instead of content:

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

The browser adds a stylesheet link to the main frame. The URL must be accessible to the browser session. If the stylesheet is private, needs authentication, or is generated dynamically, inline CSS may be a better fit. Await the call, and check the page’s console and network activity if the expected rules do not appear.

3. Style a child frame

page.addStyleTag() is a shortcut for adding a style tag to the page’s main frame. It does not target every iframe. Find the frame containing the element and call its method:

const frame = page.frames().find((candidate) => candidate.url().includes('/embedded-content'));

if (!frame) {
  throw new Error('Target frame was not found');
}

await frame.addStyleTag({
  content: '.embedded-title { color: #175cd3; }',
});

Use a frame-specific selector or URL condition that identifies the intended frame in your application. A frame may not be available until its content has loaded. The Puppeteer Frame.addStyleTag() API reference documents the frame method.

4. Choose the right page operation

Need Use Notes
Add a few rules or generated CSS to the current document page.addStyleTag({ content: css }) Creates a style element in the main frame.
Reuse a hosted stylesheet page.addStyleTag({ url: cssUrl }) Creates a stylesheet link in the main frame.
Style content inside one iframe frame.addStyleTag(...) Call it on the frame that owns the content.
Perform custom DOM work as well as styling page.evaluate() Runs a function in the browser page context; Puppeteer awaits a returned promise.
Replace the document with supplied markup page.setContent(html) Sets page content; it is not an injection method for an already loaded document.

For CSS alone, addStyleTag() is the direct API. Use evaluate() when the task involves broader page-context logic, and setContent() when you are constructing the document itself. See the official references for Page.evaluate() and Page.setContent().

5. Apply CSS before a screenshot

  1. Navigate to the target page and wait for the content you need.
  2. Call and await page.addStyleTag().
  3. Wait for any page-specific visual state needed for the capture.
  4. Take the screenshot.
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('main');
await page.addStyleTag({ content: 'header { display: none; }' });
await page.screenshot({ path: 'page.png', fullPage: true });

Navigation and site scripts can affect when a rule should be applied. If the site later changes its DOM or styles, check the resulting page and adjust the sequence for that page. The Puppeteer API documentation describes the injection operation; it does not guarantee a universal result for every site or navigation sequence.

6. Troubleshooting

Symptom Likely cause Fix
The screenshot has the original styling The injection was not awaited, happened before navigation, or the screenshot ran before injection completed. Navigate first, await page.addStyleTag(), then capture.
An iframe element is unchanged The CSS was added to the main frame while the target belongs to a child frame. Find the owning frame and call frame.addStyleTag().
URL-based CSS has no effect The browser could not load the stylesheet, or the URL is not the stylesheet you intended. Check the URL and browser network/console output, or pass the CSS through content.
A selector matches nothing The relevant element has not appeared yet, or the selector does not match the document. Wait for the element with page.waitForSelector() and verify the selector in the correct frame.
The rules work and then stop applying Later navigation or site behavior changed the document or its visual state. Apply the stylesheet after the relevant navigation or state change, then capture.

7. Reliability, performance, and cost

For a small set of rules, inline content avoids relying on a separate stylesheet request. A URL is convenient for a shared stylesheet, but its availability and loading are an additional dependency. Puppeteer’s API reference does not provide comparative performance figures, so measure the whole capture flow if latency matters.

Await each dependent browser operation and close the browser in a finally block, as in the runnable example, so errors do not leave the process running. For repeated captures, reuse browser processes where appropriate in your application and create pages per capture; account for the browser’s resource use and the target site’s response time. Your costs depend on where and how you run Puppeteer and are separate from the CSS injection API.

8. Or skip the browser setup

If you need a screenshot without managing Puppeteer and a browser, ScreenshotNeo provides a website screenshot API and MCP server for developers. One GET request returns an image or PDF; the API accepts common screenshot parameter names, which can make switching easier. See the ScreenshotNeo site and API documentation.

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,
)
open("shot.webp", "wb").write(r.content)
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}`);
}

const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', image));
  • Cookie and consent banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets can be removed; each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers report the page verdict and billing status.
  • An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
  • The free plan includes 1,000 screenshots a 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 addStyleTag return the inserted element?

Yes. It resolves to a handle for the inserted style or link element.

Can I use it on a specific iframe?

Yes. Get the corresponding Puppeteer frame and call frame.addStyleTag().

Should I use addStyleTag or setContent?

Use addStyleTag() to add CSS to an existing document. Use setContent() when supplying the document markup.