How to Add CSS to a Frame in Puppeteer
Use Puppeteer’s Frame.addStyleTag() to inject CSS into the document that owns your target frame. Learn how to find frames, load CSS, and fix common failures.
To add CSS to an iframe in Puppeteer, find the Frame that owns the document you want to style, then await frame.addStyleTag({ content: cssText }). Use page.addStyleTag() only when you want to style the main frame: Puppeteer documents that method as a shortcut for page.mainFrame().addStyleTag(). Frame.addStyleTag() accepts inline CSS, a local file path, or a stylesheet URL. See its options.
The important part is choosing the right frame. A page can contain multiple nested frames, each with its own document and JavaScript context. Styling one frame does not style its children or its parent. Puppeteer’s Frame reference describes frames as similar to iframe elements and notes that JavaScript in one frame does not affect nested frames.
Runnable example: find a frame and inject CSS
This CommonJS example launches Chromium, opens a page, finds a frame by URL, injects CSS, and closes the browser. Replace the URL and frame match with values for your page.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Match a stable part of the iframe URL for your application.
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: `
body {
background: #f5f5f5 !important;
}
.notice {
color: #174ea6 !important;
}
`,
});
// Continue with assertions, interaction, or a screenshot here.
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
page.frames() returns the page’s current frames. If you know the frame object already, the minimal call is:
await frame.addStyleTag({ content: 'body { background: red; }' });
Await the call before interacting with or capturing the styled document. The method returns a promise for a handle to the inserted <style> element when using content or path, and a <link> element for url. Method reference.
Find the correct frame
Inspect the frame tree when URL matching is not enough. The main frame is available through page.mainFrame(); each frame exposes its child frames through childFrames(). Frame API.
function printFrameTree(frame, depth = 0) {
console.log(`${' '.repeat(depth)}${frame.url()}`);
for (const child of frame.childFrames()) {
printFrameTree(child, depth + 1);
}
}
printFrameTree(page.mainFrame());
You can also inspect the iframe element that owns a frame. Puppeteer’s Frame API demonstrates using the frame element and reading its name attribute. A helper can walk the current tree and select by name:
async function findFrameByName(frame, expectedName) {
for (const child of frame.childFrames()) {
const element = await child.frameElement();
const name = await element.evaluate(node => node.getAttribute('name'));
if (name === expectedName) return child;
const nestedMatch = await findFrameByName(child, expectedName);
if (nestedMatch) return nestedMatch;
}
return null;
}
const target = await findFrameByName(page.mainFrame(), 'checkout-frame');
if (!target) throw new Error('checkout-frame was not found');
await target.addStyleTag({ content: 'body { font-family: sans-serif; }' });
Frame URLs and names may be empty or change during navigation. Prefer a stable identifier specific to your page, and check that the frame exists at the time you inject the stylesheet.
Choose how to supply the CSS
| Option | Example | Use it when |
|---|---|---|
content |
{ content: 'body { color: navy; }' } |
Rules are short, generated, or stored with your script. |
path |
{ path: './styles/frame.css' } |
You maintain CSS in a local file. |
url |
{ url: 'https://example.test/frame.css' } |
You want the frame to load a hosted stylesheet through a link element. |
Inline CSS with content
await frame.addStyleTag({
content: `
.checkout-button {
background-color: #0b57d0;
color: white;
}
`,
});
This is convenient for a small override or CSS generated at runtime. Escape or validate any untrusted values before building CSS strings. A CSS value is not automatically safe just because it is passed as a JavaScript string.
Local CSS with path
await frame.addStyleTag({ path: './styles/frame.css' });
For a relative path, Puppeteer resolves the file relative to Node.js process.cwd(), the process working directory. If a script is launched from different directories, use an absolute path or resolve the file path deliberately. The options reference documents this path behavior.
Remote CSS with url
await frame.addStyleTag({ url: 'https://example.test/frame.css' });
This adds a stylesheet link in the selected frame. The browser must be able to reach the stylesheet, and the remote server’s response and browser policies must permit it to load. Use the inline or local option when you need the rules to be self-contained.
Frames, main pages, and custom insertion
page.addStyleTag(options) targets the main frame. It is the convenient choice for the top-level document:
await page.addStyleTag({ content: 'body { color: navy; }' });
For an iframe, call addStyleTag() on the iframe’s Frame object instead. Page.addStyleTag documentation.
For custom DOM insertion or logic, use frame.evaluate(), which executes in that frame’s context. Frame.evaluate documentation.
await frame.evaluate(() => {
const style = document.createElement('style');
style.textContent = 'body { color: navy; }';
document.head.append(style);
});
Use addStyleTag() for ordinary stylesheet injection. Choose evaluate() when you need to create or update nodes conditionally, target a particular element, or combine styling with other frame-local DOM work.
Timing and edge cases
- The frame has not appeared yet: wait for the iframe to be attached or for the relevant page state, then inspect
page.frames()again. A frame list is a snapshot of the current page state. - The frame navigates: navigation replaces its document. If styles disappear after navigation, find the current frame and inject the rules after the new document is ready.
- The page has nested frames: select the frame that owns the target elements. Injecting into a parent does not cross into a child frame.
- The CSS loses to page rules: inspect specificity, load order, and dynamically applied styles. Use a more specific selector or, when appropriate,
!important; do not add it indiscriminately. - The document has no head element yet: prefer
addStyleTag(), which manages stylesheet insertion, or wait for the document to reach a suitable state before custom insertion withevaluate(). - The frame is removed: a detached or replaced frame cannot be treated as the original live document. Reacquire it from the current page frame tree.
- Cross-origin iframe: browser page JavaScript cannot freely reach into a cross-origin iframe through the parent document. Puppeteer’s frame-scoped API runs in the selected frame context; the frame still needs to exist and be available to Puppeteer.
When frame selection depends on page events, avoid arbitrary long sleeps where possible. Wait for a specific iframe selector or application condition, then locate the frame and inject. The right wait condition depends on how the page creates and navigates its frames.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| No visible change | You styled the main frame instead of the iframe, or selected a different child frame. | Print the frame tree and inspect frame URLs, names, and nesting. Call addStyleTag() on the frame that owns the element. |
| “Target frame was not found” | The frame has not loaded, the match is too strict, or its URL differs from the assumed value. | Wait for the iframe to appear, log page.frames().map(frame => frame.url()), and use a stable matching condition. |
| File path error | The relative CSS path is resolved from process.cwd(), which differs from the script directory. |
Check the process working directory and file existence; use an absolute path when the launch directory may vary. |
| Styles vanish after navigation | The frame navigated and created a new document. | Wait for the new document, reacquire the current frame, and inject the CSS again. |
| Remote stylesheet has no effect | The URL is unreachable, returns an error, or is blocked by the page or browser’s loading policy. | Check the stylesheet response and browser console/network errors. Try content or path to isolate a loading issue. |
evaluate() reports a detached frame |
The frame was removed or replaced between selection and evaluation. | Find the frame again from the page’s current frame tree, then retry at the right page lifecycle point. |
| Injection call rejects | The frame may have navigated, detached, or closed while the operation was pending. | Confirm page and browser lifecycle, reacquire the frame after navigation, and await injection before continuing. |
Performance, reliability, and cost
Adding a small inline stylesheet is usually a small step in a browser automation flow; the larger cost is typically launching and keeping a browser open and loading the target page. This is a practical expectation, not a published benchmark. Reuse a browser for related work where your application’s isolation requirements allow it, and close it in a finally block so failures do not leave browser processes running.
For repeatable captures, inject after the target frame’s document is ready, and reacquire the frame after navigation. Keep CSS scoped to the frame’s document and selectors you intend to change. For remote CSS, availability depends on another network request; inline or local CSS avoids that stylesheet fetch. Puppeteer itself does not charge per CSS injection; your compute, browser hosting, and network costs depend on where and how you run automation.
Or skip the browser setup
If the goal is a clean screenshot of a web page rather than custom browser automation, ScreenshotNeo is a website screenshot API and MCP server. Its one-call API can return an image or PDF without launching and maintaining Puppeteer in your application. See the ScreenshotNeo 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,
)
r.raise_for_status()
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}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. 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.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does addStyleTag return the inserted element?
Yes. It resolves to a handle for the inserted style element with content or path, and a link element with url. You can keep the handle if you need to inspect or remove the node.
Can I use a CSS selector to choose the frame?
Use the iframe element selector to identify the embedding element, then map it to its frame using Puppeteer’s frame APIs. For pages with multiple or nested frames, verify the selected frame’s URL or name before injecting.
Can this change the website for other visitors?
No. The injected stylesheet affects the document in the browser session controlled by Puppeteer. It does not publish CSS to the website or change what other visitors receive.


