How to Add Custom CSS Before Taking a Playwright Screenshot
Add CSS before a Playwright screenshot with page.addStyleTag, or apply it only during capture. Includes runnable examples, iframe guidance, and fixes for common issues.
To change the page before taking a regular Playwright screenshot, await page.addStyleTag() after navigating and before calling page.screenshot(). Pass CSS with the content option:
await page.addStyleTag({
content: '.cookie-banner { display: none !important; }'
});
await page.screenshot({ path: 'page.png' });
If the CSS should affect only the screenshot, use page.screenshot({ style: css }) instead. For a Playwright Test visual assertion using a stylesheet file, use toHaveScreenshot({ stylePath }). Both screenshot-time options were added in Playwright v1.41. See the official Page API, visual comparisons guide, and screenshot assertion API.
1. Choose where the CSS should apply
| Approach | Best for | CSS input |
|---|---|---|
page.addStyleTag() |
Changing the page document before a regular capture | Inline content, local file path, or stylesheet URL |
page.screenshot({ style }) |
Temporary capture-only changes | CSS string |
expect(page).toHaveScreenshot({ stylePath }) |
Playwright Test visual assertions with a maintained stylesheet | Stylesheet path |
Use addStyleTag when you need the modified style to be present on the page as it proceeds through your capture workflow. Use the screenshot style option when the adjustment should be limited to capture time, such as hiding a timestamp or animation that makes the image unstable.
2. Add inline CSS before a regular screenshot
This complete Node.js example uses Playwright’s browser package. Install it with npm install playwright, then install a browser with npx playwright install chromium. Save the code as screenshot.mjs and run node screenshot.mjs.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.addStyleTag({
content: `
.cookie-banner,
.newsletter-modal {
display: none !important;
}
body {
background: #fff !important;
}
`,
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
addStyleTag returns after the stylesheet has been loaded or the CSS injected, so await it before capturing. The method accepts one of these forms:
await page.addStyleTag({ content: 'body { background: white; }' });
await page.addStyleTag({ path: './capture.css' });
await page.addStyleTag({ url: 'https://example.com/capture.css' });
For repeatable scripts, a local path keeps the capture CSS alongside the code. A remote url is useful when the stylesheet is hosted centrally, but it adds a network dependency. Inline content is convenient for small, capture-specific rules.
3. Apply CSS only while taking the screenshot
Use the screenshot-time style option when you do not want to modify the page for the rest of the script. It accepts a CSS string:
const css = `
.cookie-banner, .chat-widget { display: none !important; }
[data-testid="live-clock"] { visibility: hidden !important; }
`;
await page.screenshot({
path: 'page.png',
fullPage: true,
style: css,
});
This option is documented for hiding dynamic elements and changing properties to make screenshots repeatable. It is not a substitute for waiting until the application has rendered the content you want to keep.
4. Use a CSS file with Playwright Test
For a visual assertion, put the capture-only rules in a stylesheet and pass its path as stylePath. The assertion API works with the Playwright Test runner.
/* screenshot.css */
.cookie-banner,
.chat-widget {
display: none !important;
}
[data-testid="live-clock"] {
visibility: hidden !important;
}
// example.spec.ts
import { test, expect } from '@playwright/test';
test('page screenshot', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot({ stylePath: './screenshot.css' });
});
Install the test package with npm install -D @playwright/test, install the browser with npx playwright install chromium, and run the test with npx playwright test. The assertion waits for two consecutive screenshots to match before comparing against the expected image. This helps settle page rendering, but does not make rendering identical across different machines or browser environments.
Keep the stylesheet path valid relative to the test’s working context, and commit the CSS file with the test so the capture rules are available wherever the test runs.
5. Target content inside an iframe
Page-level styling may not reach content inside an iframe the way you expect. Locate the frame and call frame.addStyleTag() to inject CSS into that frame’s document:
const frame = page.frameLocator('#embedded-content');
// For a known frame object, use page.frame(...) and then frame.addStyleTag(...).
const targetFrame = page.frame({ name: 'embedded-content' });
if (!targetFrame) throw new Error('Embedded frame was not found');
await targetFrame.addStyleTag({
content: '.third-party-banner { display: none !important; }',
});
await page.screenshot({ path: 'page.png' });
The Page API’s screenshot-time style option applies across inner frames according to its documentation. When you need to modify a particular frame’s document before capture, use that frame’s addStyleTag method. Cross-origin restrictions still affect JavaScript access to frame contents; Playwright’s frame APIs are the supported way to work with frames.
6. Make captures reliable and repeatable
- Wait for the right page state. Navigate, then wait for an application-specific locator or state before styling and capturing. A stylesheet cannot fix a page that has not finished rendering the content.
- Inject CSS after navigation. A navigation replaces the document, so styles injected into the old document are lost. Add them after the final navigation.
- Use specific selectors. Prefer stable classes, IDs, or test IDs. Broad rules such as
div { display: none }can remove content needed in the image. - Use
!importantselectively. It helps override site rules for a capture, but can conceal a selector mismatch. Inspect the target element when a rule appears ineffective. - Control the environment for visual tests. The official guide notes that rendering can vary with operating system, browser version, settings, hardware, power source, and headless mode. Generate and compare baselines in the same environment where practical.
- Keep CSS deterministic. Hide or normalize changing content such as clocks, rotating banners, and animations if those changes are irrelevant to the assertion.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot looks unchanged | CSS was injected before a navigation, the selector does not match, or a later rule overrides it | Inject after the final navigation, verify the selector against the rendered DOM, and raise specificity or use !important where appropriate. |
| The screenshot is missing page content | A broad selector or inherited style hid more than intended | Narrow the selector and review every rule applied to the target elements. |
| A stylesheet path fails | The path is incorrect relative to the process or test context | Check the file location and use a path that resolves from the running script; use inline content to isolate path problems. |
| A stylesheet URL does not load | The browser cannot reach the host, or the remote stylesheet request fails | Check network access and the URL. For a capture that must not depend on a remote host, use inline CSS or a local file. |
| Iframe content is not styled | The CSS was added to the top-level document instead of the frame document | Get the intended frame and call its addStyleTag method. |
| Visual assertions are inconsistent across machines | Browser or host rendering differs, or dynamic content remains visible | Run on a consistent browser and host setup, and hide or normalize irrelevant dynamic content with screenshot CSS. |
style or stylePath is rejected |
The installed Playwright version predates these options | These screenshot-time options were added in Playwright v1.41. Update Playwright if you need them; page.addStyleTag() is the alternative for regular captures. |
toHaveScreenshot is unavailable |
The code is running outside the Playwright Test runner or the test package is not installed | Use @playwright/test for assertions, or use page.screenshot() in a standalone script. |
8. Performance, reliability, and cost
Injected CSS is usually a small part of capture work; navigation, page rendering, and image encoding are often the larger tasks, though the exact time depends on the site and environment. Avoid unnecessary remote stylesheet requests in latency-sensitive jobs. Reuse a browser process for multiple pages in a worker when your application architecture permits, and close pages and browsers when finished to release resources.
Local Playwright captures have no per-screenshot API charge, but you operate the browser environment and maintain its dependencies. For visual regression suites, consistent browser versions and host settings reduce unexplained diffs. If captures run at scale, account for browser CPU and memory, test retries, and the time needed to refresh baselines when the application intentionally changes.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It can apply custom CSS as part of a screenshot request, alongside options for full-page capture and other page adjustments. See the API documentation for request parameters.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
--data-urlencode 'css=body { background: #fff !important; } .cookie-banner { display: none !important; }' \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com",
"css": "body { background: #fff !important; } .cookie-banner { display: none !important; }",
},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
css: 'body { background: #fff !important; } .cookie-banner { display: none !important; }',
});
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);
Cookie banners are accepted before capture, and known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers reporting the page verdict and billing status. AI agents can use its MCP server tools, including take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
10. FAQ
Does adding a style tag change the live website?
It changes the document open in the Playwright page. It does not publish CSS to the website’s server.
Can I use a stylesheet file for a normal screenshot?
Yes. Pass its path to page.addStyleTag({ path }). For a Playwright Test screenshot assertion, use stylePath.
Does CSS make screenshots identical on every machine?
No. CSS can control page-level content and styling, but browser and host rendering can still vary. Keep screenshot comparisons in a consistent environment.
Should I use addStyleTag or screenshot-time CSS?
Use addStyleTag when you want the page document styled before capture. Use screenshot-time CSS when the change should apply only to the image or visual assertion.


