How to Add Custom CSS Before Capturing a Website Screenshot
Inject CSS before a Playwright or Puppeteer screenshot to hide elements or stabilize a page. Learn when to use Playwright Test’s capture-only stylesheet too.
To add custom CSS before a website screenshot in Playwright, navigate to the page, inject the stylesheet with page.addStyleTag(), wait for the page state you need, and capture it. Use Playwright Test’s stylePath option when the CSS should apply only while a visual snapshot is taken. Puppeteer also supports page-level CSS injection with page.addStyleTag().
1. Choose where the CSS should apply
| Method | Best for | Effect |
|---|---|---|
Playwright page.addStyleTag() |
General browser automation; styling the page before capture or inspecting the changed page | Adds a style element or stylesheet to the page, affecting subsequent page actions. |
Playwright Test stylePath |
Visual snapshots where screenshot-specific CSS should not become part of later page interactions | Applies a custom stylesheet during the screenshot operation. |
Puppeteer page.addStyleTag() |
General browser automation using Puppeteer | Adds a style element or stylesheet to the page. |
For page-level injection, add the CSS after navigation and before the screenshot. If the page changes asynchronously, wait for the specific content or state your workflow needs before capturing. There is no single wait strategy that fits every website.
2. Playwright: inject inline CSS before capture
This runnable example uses Playwright’s JavaScript API. Install Playwright and its browser with npm install -D playwright and npx playwright install chromium, then save the following as screenshot.mjs and run node screenshot.mjs.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.addStyleTag({
content: `
.cookie-banner,
.newsletter-popup,
.chat-widget {
display: none !important;
}
body {
background: #fff !important;
}
`,
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
The content value is raw CSS. Use selectors that match the target site and !important when the page’s own styles would otherwise override the screenshot rule. Playwright also accepts a local stylesheet path or a URL in addStyleTag(); see the Playwright Page API.
Use a CSS file or stylesheet URL
For reusable rules, keep the stylesheet separate. A local file can be injected like this:
await page.addStyleTag({ path: './screenshot.css' });
Or load a stylesheet by URL:
await page.addStyleTag({ url: 'https://example.com/screenshot.css' });
Keep the file or URL available to the process and ensure the page can load it. Inline content avoids an extra stylesheet fetch and is convenient for a short, one-off rule set.
3. Playwright Test: apply CSS only to a visual snapshot
When CSS is only for a screenshot comparison—for example, to hide a changing timestamp—use Playwright Test’s stylePath. The stylesheet is applied while taking the screenshot, which helps keep screenshot-specific changes out of the normal page interaction flow. Playwright documents this option in its visual comparisons guide.
Create screenshot.css:
iframe,
.cookie-banner,
.last-updated-time {
visibility: hidden;
}
Then pass its path to the snapshot assertion:
import path from 'node:path';
import { test, expect } from '@playwright/test';
test('page visual snapshot', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot({
stylePath: path.join(process.cwd(), 'screenshot.css'),
});
});
Choose the CSS declaration to match the goal. display: none removes an element from layout; visibility: hidden hides it while preserving its layout space. For visual comparisons, preserving layout can prevent the rest of the page from shifting.
4. Puppeteer: inject CSS before capture
Puppeteer uses the same page-level pattern: navigate, add a style tag, and take the screenshot. Install Puppeteer with npm install puppeteer.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.addStyleTag({
content: '.cookie-banner { display: none !important; }',
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Puppeteer documents this method in its Page.addStyleTag() API reference.
5. Make screenshot CSS reliable
Wait for the page state you intend to capture
Navigation completing does not necessarily mean a single-page app has finished rendering, or that a popup has appeared. Wait for a site-specific selector when possible, then inject CSS and capture:
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('main').waitFor({ state: 'visible' });
await page.addStyleTag({
content: '.cookie-banner { display: none !important; }',
});
await page.screenshot({ path: 'page.png' });
If the element you need is rendered later, wait for that element or the application state that signals it is ready. A fixed delay can work for a known delay, but is usually less dependable than waiting for a meaningful page condition.
Use the right selector and CSS behavior
- Check that the selector matches the element on the actual page. Class names can differ by route, locale, or experiment.
- Use
display: none !importantto remove an unwanted overlay and its layout box. - Use
visibility: hiddenwhen you want to preserve the element’s space and avoid shifting nearby content. - Set dimensions, colors, or other presentation rules explicitly when page styles or themes make the result variable.
- Prefer narrow selectors. A broad rule such as
div { display: none }can hide essential content.
Full-page screenshots and lazy content
A full-page screenshot can include content that is loaded as you scroll. If lower sections are blank or incomplete, make the page load the content first, then apply screenshot CSS and capture. The right scroll or loading behavior depends on the site; verify the resulting image rather than assuming navigation alone loaded every section.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The element is still visible | The selector does not match, the element appears after injection, or a more specific rule overrides yours. | Inspect the live page, use a selector that matches the rendered element, wait for it to appear, and try !important. |
| The page layout jumps after hiding an element | display: none removes the element’s space. |
Use visibility: hidden if preserving its layout space gives the desired composition. |
| The CSS file is not applied | The path or URL is wrong, the file is unavailable, or the stylesheet request fails. | Check the path relative to the running process, confirm the file exists, or use inline content to isolate the issue. |
| The page looks different between runs | Dynamic content, animations, delayed rendering, or changing data affects the capture. | Wait for the relevant page state and use screenshot-specific CSS to hide volatile regions. For Playwright Test, consider stylePath. |
| The CSS works in the browser but not in the screenshot | Injection may happen before the target is rendered, or the captured page/state differs from the inspected one. | Wait for the target selector, inject after navigation and page readiness, then capture that same page state. |
| A full-page image is missing lower content | Lazy-loaded sections may not have rendered before capture. | Trigger the site’s required loading or scroll behavior, wait for content, then take the full-page screenshot. |
| Important page content disappears | The selector is too broad or matches a shared container. | Narrow the selector and check the screenshot after each CSS change. |
7. Performance, reliability, and cost
A small inline stylesheet adds little setup compared with launching a browser and loading a page. A stylesheet URL introduces a fetch that must complete for the rules to apply; a local path or inline content avoids depending on a remote stylesheet host. Browser startup, the target site’s response, and page rendering usually determine how long the overall capture takes.
For repeatable captures, keep the viewport, page state, selector rules, and wait condition consistent. Hide or normalize only the portions that vary and matter to the comparison. Screenshot CSS changes what is rendered; it does not make the underlying page data or network behavior deterministic.
With Playwright or Puppeteer, your costs depend on where and how you run the browser and on the pages you load. Consider the browser runtime, concurrency, retries, and storage for generated images when estimating a production workflow.
8. Or skip the browser setup
If you need a screenshot without running browser automation, ScreenshotNeo is a website screenshot API and MCP server. Its API can apply custom CSS as part of a screenshot request. See the ScreenshotNeo API documentation for the request options.
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",
"css": "body { background: #fff; }",
},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
css: 'body { background: #fff; }',
});
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', res);
Use the CSS parameter for your desired rules and consult the docs for its exact request name and available options. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
9. FAQ
Can I use a stylesheet URL instead of writing inline CSS?
Yes. Playwright and Puppeteer page-level injection support adding a stylesheet by URL, and Playwright also supports a local CSS file path.
Will injected CSS change the live website?
It changes the page rendered in your automation browser. It does not edit the website’s source files or publish a change to the site.
Which method should I use for screenshot tests?
Use Playwright Test’s stylePath for CSS that should affect only the screenshot operation. Use addStyleTag() when later automation steps should see the injected styling or you need to inspect the modified page.
Can CSS remove a cookie banner before capture?
It can hide a banner whose selector you know. If the site renders a different banner or creates it after injection, wait for the relevant state and target the rendered element.


