CSS & JS Injection

Inject custom CSS and JavaScript into any page before the screenshot is taken. Hide elements, change fonts, expand accordions, fill forms — all with a single API call.

Vitalii Holben Updated

Taking screenshots through an API sounds straightforward until you actually try it. Default settings almost never give you exactly what you need. A cookie banner covers half the page. A sticky header ends up in the middle of a full-page capture. The font rendering looks different from what you expected. A chat widget sits on top of the content you're trying to show. The page itself is fine, but the screenshot isn't.

Fine-tuning captures is detailed work, and getting a clean result on the first try is rare. That's why ScreenshotRun lets you inject custom CSS and JavaScript into any page before the screenshot is taken. The css parameter adds a <style> tag to the page. The js parameter runs JavaScript with full DOM access. Both execute after the page loads but before the capture, so you can reshape the page however you want without touching the source code.

What CSS injection actually does

The css parameter takes a string of CSS and injects it as a <style> tag into the page's <head>. Your styles layer on top of the page's existing CSS, so you can override anything with enough specificity. Add !important if the page's styles fight back.

curl "https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&css=.sidebar+%7B+display:+none+%7D+.main+%7B+width:+100%25+%7D&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY"

That request hides the sidebar and stretches the main content to full width. The page itself doesn't change, only the screenshot does. A few things I find myself using CSS injection for most often: hiding sticky headers that follow you down the page and clutter full-page captures, removing promotional banners that aren't part of the actual content, overriding fonts when generating thumbnails across multiple sites (so they all look consistent), and forcing a fixed width on fluid layouts that would otherwise render differently depending on viewport.

CSS injection runs after the page loads and after any built-in blocking (block_ads, block_cookies, block_chats). So if you're already using those parameters to remove common annoyances, CSS injection is for the custom stuff they don't cover.

What JavaScript injection does

The js parameter runs JavaScript in the page's browser context, with full access to the DOM. It runs before CSS injection, so your script can restructure the page before styles are applied. You can use async/await if you need to wait for something.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "url=https://example.com/faq" \
  --data-urlencode 'js=document.querySelectorAll("details").forEach(d => d.open = true);' \
  --data-urlencode "full_page=true" \
  --data-urlencode "format=png"

That opens every <details> accordion on a FAQ page before capturing. Without it, the screenshot would show collapsed sections with just the headings visible.

JavaScript injection goes further than CSS can. You can click buttons to reveal hidden content, remove elements from the DOM entirely (not just hide them visually), fill form fields with test data and trigger input events so the UI reacts, scroll to a specific section, pause videos, dismiss modals, or even rewrite text on the page. Anywhere you'd open DevTools and type something into the console, the js parameter does the same thing.

Execution order

The renderer runs everything in a specific sequence. Knowing the order matters when you combine multiple parameters, because a CSS rule can't hide an element that JS already removed, and JS can't interact with elements that haven't loaded yet.

  1. Page loads (waits for network idle)
  2. Built-in blocking runs: block_ads, block_cookies, block_chats
  3. Your JavaScript runs (js parameter)
  4. Your CSS is injected (css parameter)
  5. hide_selectors removes matching elements
  6. click_selector / hover_selector interaction
  7. delay waits the specified seconds
  8. Screenshot is captured

JS runs before CSS intentionally. If your script adds or modifies DOM elements, your CSS can target those new elements. And if your JS removes something from the page, your CSS doesn't need to worry about hiding it.

CSS injection vs. hide_selectors vs. block_ads

Three parameters can remove things from a page, and they work differently. Choosing the right one saves you from writing CSS or JS you don't need.

block_ads, block_cookies, and block_chats are the simplest. They're booleans: flip them on and the renderer handles common ad networks, cookie consent popups, and chat widgets automatically. No selectors to write, no CSS to maintain. If the thing you want removed falls into one of these categories, use these first.

hide_selectors takes an array of up to 20 CSS selectors and does three things: it hides matching elements with CSS (display:none !important), it removes them from the DOM entirely, and it sets up a MutationObserver (a browser API that fires the moment something changes on the page) to catch elements that appear dynamically after page load. It's more aggressive than CSS injection because the elements are actually removed, not just hidden. Use it when you know the exact selector of something specific to that site, like #promo-banner or .sidebar-newsletter.

CSS injection gives you the most control. You can hide elements, but you can also change fonts, adjust layouts, override colors, add borders for debugging, set background transparency, and do anything else CSS can do. Use it when you need to restyle the page, not just remove parts of it.

Quick rule: if it's a common annoyance, use the block_* booleans. If it's a specific element you want gone, use hide_selectors. If you need to change how something looks rather than remove it, use CSS injection.

Real examples that come up in practice

Agencies often need screenshots of client websites for reporting, but the screenshots need to look clean. Hide the cookie banner, the chat widget, and the sticky nav, then stretch the content area.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "url=https://client-site.com" \
  --data-urlencode "block_cookies=true" \
  --data-urlencode "block_chats=true" \
  --data-urlencode "css=header.sticky { position: relative !important; } .promo-bar { display: none; }" \
  --data-urlencode "full_page=true" \
  --data-urlencode "format=png"

Documentation pages love collapsible sections. If you're archiving or comparing docs, you need everything expanded. This script opens both <details> elements and any custom accordion that uses aria-expanded. The delay=1 gives the content time to render after expanding.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "url=https://docs.example.com/api-reference" \
  --data-urlencode 'js=document.querySelectorAll("details").forEach(d => d.open = true); document.querySelectorAll("[aria-expanded=false]").forEach(el => el.click());' \
  --data-urlencode "full_page=true" \
  --data-urlencode "delay=1" \
  --data-urlencode "format=png"

You can also fill a search form and capture the results page without ever opening a browser. This fills the search field, dispatches an input event (so React/Vue picks it up), submits the form, and waits 3 seconds for results to load.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "url=https://example.com/search" \
  --data-urlencode 'js=const input = document.querySelector("#search-input"); input.value = "screenshot api"; input.dispatchEvent(new Event("input", { bubbles: true })); document.querySelector("#search-form").submit();' \
  --data-urlencode "delay=3" \
  --data-urlencode "format=png"

When you need a screenshot with no background for a product mockup or pitch deck, omit_background removes the default white. But some pages set their own background color in CSS, which overrides it. CSS injection fixes that.

curl "https://api.screenshotrun.com/v1/screenshots/capture" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -G \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "omit_background=true" \
  --data-urlencode "css=html, body { background: transparent !important; }" \
  --data-urlencode "format=png"

Combining CSS and JS with other parameters

CSS and JS injection pair naturally with the rest of the API. Use css with full-page screenshots to clean up sticky headers that would repeat on every viewport slice. Use js with wait_for_selector when your script triggers content that needs time to render. Pair both with dark mode when you want to capture the dark theme but need to hide elements that only appear in dark mode. And if the target site blocks your injected <style> tag with a Content-Security-Policy header, add bypass_csp=true to override the restriction.

For certified screenshots, keep in mind that the certification hash covers the final rendered page after your CSS and JS have run. The options_hash includes your injection parameters, so the same URL with different CSS produces a different certification fingerprint. This is correct behavior, but worth knowing if you're using certified captures for compliance.

The css and js parameters

ParameterTypeDefaultMaxPlanDescription
cssstring—10,000 charsPro+CSS injected as a <style> tag into the page before capture.
jsstring—10,000 charsPro+JavaScript executed in the page context before capture. Runs before CSS. Supports async/await.
bypass_cspbooleanfalse—Pro+Bypass Content-Security-Policy headers. Required for CSS/JS injection on CSP-protected pages.

CSS and JS injection are available on Pro ($29/mo), Growth ($79/mo), and Business ($119/mo) plans. See the full CSS & JS injection docs for more examples, including custom HTTP headers and cookies.

Limits and things to watch out for

Both parameters cap at 10,000 characters. That's enough for most use cases, but if you're injecting an entire CSS framework or a large script, you'll hit the limit. The workaround is to host your CSS/JS externally and inject a small loader script that fetches it, though this adds a network request and needs bypass_csp if the page has a restrictive CSP.

If your injected JavaScript throws an error, the entire screenshot request fails. There's no try-catch wrapper around your code, so add your own error handling for anything that might not exist on the page. A safe pattern is optional chaining: document.querySelector("#maybe-exists")?.remove() won't throw if the element isn't there.

Content-Security-Policy headers can block your injected styles and scripts. The browser treats injected <style> tags and eval() calls the same way it treats inline scripts, so a strict CSP will reject them. Add bypass_csp=true to your request to override this. Most public websites don't have CSPs strict enough to block style injection, but internal apps and banking sites often do.

One more thing: JS injection runs synchronously by default. If your script returns a promise, the renderer waits for it to resolve, so async code works fine. But there's no separate timeout for your script. If it hangs (infinite loop, unresolved promise), the only thing that stops it is the overall request timeout.

FAQ#

Yes. JavaScript runs first, then CSS. This lets your JS script modify the DOM while CSS styles the new or changed elements.

The screenshot request fails. The renderer doesn't wrap your code in try-catch. Use optional chaining (?.remove()) and your own error handling for elements that might not exist on the page.

hide_selectors removes elements from the DOM entirely and watches for dynamically appearing ones via MutationObserver. CSS injection gives full control: you can change fonts, colors, layouts, and anything else CSS can do, not just hide things.

Only if the target site uses a strict Content-Security-Policy. Most public websites don't block injected styles, but internal apps and banking sites often do. Add bypass_csp=true in those cases.

10,000 characters each. For larger scripts, host the code externally and inject a small loader that fetches it. This adds a network request and may require bypass_csp=true if the page has a restrictive CSP.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call