How to Fix Playwright Screenshots That Include a Scrollbar
Hide an unwanted scrollbar with screenshot-only CSS, or capture the correct scroll container when you need one to appear.
If an unwanted scrollbar appears in a Playwright screenshot, use the screenshot API’s style option to apply CSS only during capture. Put the rule on the element that actually scrolls: often the document root for page scrolling, or a specific panel for nested scrolling. If the scrollbar is missing and you want it, first check whether you are taking a viewport, full-page, or element screenshot; a full-page capture covers the document’s scrollable content but does not expand nested scrollable panels.
1. Identify which scrollbar and screenshot you mean
Before changing CSS, establish the scroll owner and desired result. A scrollbar can belong to the document or to a nested element such as a sidebar, table, or chat panel. The appropriate selector and capture scope depend on that distinction.
| Capture | What it captures | Scrollbar implications |
|---|---|---|
| Viewport screenshot | The currently visible browser viewport. | Shows what is rendered in that viewport, subject to the browser and operating system’s scrollbar behavior. |
| Full-page screenshot | The document’s full scrollable content. | It is not a sequence of viewport screenshots and does not expand nested scrollable elements. |
| Element screenshot | A selected element’s rendered bounds. | Useful for a nested panel; capture the panel directly if you want its content and scrollbar in the image. |
Playwright documents fullPage as capturing the full scrollable page, with a default of false, and documents screenshot-time style for applying a stylesheet during capture. A Playwright collaborator also clarifies that full-page capture renders the document’s scrollable content rather than expanding nested scroll containers. Playwright screenshot documentation · Playwright issue discussion on nested scroll containers.
2. Hide an unwanted scrollbar with screenshot-only CSS
Use page.screenshot({ style }) to inject a stylesheet for the screenshot. Scope the CSS to the real scroll owner; do not assume a rule for body will affect a panel whose own overflow property creates scrolling.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Replace this selector with the element that owns the unwanted scrolling.
const scrollOwner = 'html';
await page.screenshot({
path: 'screenshot.png',
fullPage: true,
style: `${scrollOwner} { scrollbar-width: none; }
${scrollOwner}::-webkit-scrollbar { display: none; }`,
});
} finally {
await browser.close();
}
The CSS shown covers common standards-based and WebKit-style scrollbar selectors, but the research does not establish a universal cross-browser recipe. Check the screenshot in the browser engine and environment you use. For document scrolling, the scroll owner may be html, body, or both depending on the page. For a nested panel, set scrollOwner to that panel’s selector, for example .results-pane. Avoid applying a broad rule to every element unless you intend to hide all scrollbars in the captured page.
The style option changes styling during screenshot rendering, so this approach does not require editing the application’s permanent stylesheet. Playwright’s API also accepts a stylesheet as a string; see the Page screenshot API reference.
Hide only one nested panel’s scrollbar
const panelSelector = '.results-pane';
await page.locator(panelSelector).screenshot({
path: 'panel.png',
style: `${panelSelector} { scrollbar-width: none; }
${panelSelector}::-webkit-scrollbar { display: none; }`,
});
Use a locator screenshot when you want the panel’s rendered bounds. Screenshot-time style applies for capture; it should not be treated as a permanent application change.
3. If you want the scrollbar to appear
First choose the capture scope that matches the scroll owner. To capture the document’s full content, use fullPage: true. To capture a nested scrolling region, target that element itself. A full-page screenshot does not turn each nested panel into a full-height document.
// Document content
await page.screenshot({ path: 'document.png', fullPage: true });
// A nested scrollable panel
await page.locator('.results-pane').screenshot({ path: 'results-pane.png' });
Whether a visible scrollbar is painted in the resulting image can depend on browser and operating-system behavior. One Playwright issue reports an absent body scrollbar in a particular Windows 11 and Playwright 1.50.1 setup; treat issue reports as environment-specific diagnostics, not a general guarantee. If you need a scrollbar represented consistently in a design image, consider drawing it as part of the page or adding screenshot-only styling for a custom scrollbar after confirming the target browser behavior.
4. Complete example in Python
Install Playwright and its browser binaries, then run this script. Change SCROLL_OWNER to the element that owns the scrollbar, and change the CSS if your target engine needs a different rule.
# pip install playwright
# playwright install chromium
from playwright.sync_api import sync_playwright
URL = "https://example.com"
SCROLL_OWNER = "html" # Or a nested scroller such as ".results-pane"
css = f"""
{SCROLL_OWNER} {{ scrollbar-width: none; }}
{SCROLL_OWNER}::-webkit-scrollbar {{ display: none; }}
"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900})
try:
page.goto(URL, wait_until="networkidle")
page.screenshot(
path="screenshot.png",
full_page=True,
style=css,
)
finally:
browser.close()
5. Complete example in Node.js
// npm install playwright
// npx playwright install chromium
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const scrollOwner = 'html'; // Or a nested scroller, such as '.results-pane'
const style = `${scrollOwner} { scrollbar-width: none; }
${scrollOwner}::-webkit-scrollbar { display: none; }`;
try {
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({
path: 'screenshot.png',
fullPage: true,
style,
});
} finally {
await browser.close();
}
})();
6. Complete example in cURL and Python with ScreenshotNeo
For a hosted screenshot API rather than managing a browser locally, ScreenshotNeo returns a screenshot or PDF from one GET request. Its documented CSS option can be used to apply capture-specific styling; consult the ScreenshotNeo API documentation for the supported request parameters.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d css="html { scrollbar-width: none; } html::-webkit-scrollbar { display: none; }" \
-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": "html { scrollbar-width: none; } html::-webkit-scrollbar { display: none; }",
},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
css: 'html { scrollbar-width: none; } html::-webkit-scrollbar { display: none; }',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The scrollbar remains after injecting CSS. | The selector does not match the element that owns scrolling, or the target browser does not apply that scrollbar rule. | Inspect which element has the scrollable overflow, scope the style to it, and verify the CSS in the same browser engine used for capture. |
| The page scrollbar disappears but a panel scrollbar remains. | The panel has its own scrolling and is a separate scroll owner. | Apply the screenshot style to the panel selector too, or capture the panel directly if it is the desired output. |
| A full-page image does not show a nested panel’s full contents. | fullPage covers document scrolling, not nested scroll containers. |
Capture the panel element; if the goal is all its content, arrange for the panel to expand or capture its contents through a page-specific method. |
| A full-page image has no document scrollbar. | Full-page capture represents the document’s scrollable content and may not render the scrollbar as expected in a given engine or environment. | Check a viewport capture and your browser/OS combination. Do not assume full-page mode guarantees a visible bar. |
The page times out waiting for networkidle. |
Some pages keep network connections active or continue background requests. | Use an appropriate readiness condition for the page, such as domcontentloaded, then explicitly wait for the content or selector needed in the screenshot. |
| The screenshot is unexpectedly clipped. | The selected element’s bounds or document capture scope differs from the intended output. | Confirm the locator and choose viewport, full-page, or element capture deliberately; for panels, inspect the element’s dimensions and scroll behavior. |
Reports involving --hide-scrollbars show that expectations differ across screenshot modes. Do not rely on that launch argument as a universal solution; use screenshot-time CSS for the hide case and choose the correct capture target for the show case. Example environment-specific scrollbar report · Nested scroll container discussion.
8. Performance, reliability, and cost considerations
Adding a short stylesheet at screenshot time is generally a small part of the capture workflow, but actual capture time depends on navigation, page readiness, and rendering; the cited sources provide no benchmark for scrollbar-specific styling. Reusing a browser instance for multiple captures can avoid repeated browser startup in a batch workflow, while closing pages and browsers reliably prevents resource leaks.
For repeatable output, pin the Playwright version and browser engine in your project, use a fixed viewport, and test on the target operating system. Browser and OS differences can affect whether scrollbars are painted, so do not infer that one issue report defines all environments. The browser automation route has infrastructure and maintenance costs of its own; a hosted API trades local browser setup for per-plan request limits. ScreenshotNeo’s free plan includes 1,000 screenshots per month with no card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
9. Or skip the browser setup
ScreenshotNeo accepts a URL and returns a screenshot, so you can avoid installing and maintaining a browser for this capture. The CSS above is passed with the request to hide the document scrollbar; adjust the selector for your page’s actual scroll owner. Its capture cleanup accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan gives 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Read the API documentation and ScreenshotNeo overview, then sign up free for 1,000 screenshots a month with no card.
FAQ
Does fullPage: true mean every scrollbar is included?
No. It captures the document’s full scrollable content; it does not expand nested scrolling regions, and visible scrollbar rendering can vary by environment.
Will hiding the scrollbar stop the page from scrolling?
The CSS shown targets scrollbar rendering. Verify behavior in your target browser and avoid replacing it with rules that disable overflow unless you also intend to disable scrolling.
Should I use body or html?
Use the element that owns scrolling on the page being captured. Inspect the page rather than assuming one selector applies everywhere.


