Puppeteer Screenshot Has the Wrong Page Width: Fix Horizontal Overflow
Find out whether Puppeteer’s viewport, screenshot options, or page layout is making your screenshot too wide, then isolate and fix the cause.
A Puppeteer screenshot can look too wide for three different reasons: the configured CSS viewport is wider than intended, the screenshot captures a region beyond that viewport, or the rendered page has horizontal overflow. Measure each separately before changing CSS.
For a viewport screenshot, set the viewport before navigation, omit fullPage, and check the page’s actual runtime dimensions. Compare the document’s scrollWidth with its clientWidth to detect layout overflow. The specific overflowing element depends on the affected page and cannot be identified without its URL or reproduction code.
1. Tell viewport width from image width
CSS viewport width and saved bitmap width are different measurements. A 1280 CSS-pixel viewport can produce an image with a different pixel width when the device scale factor is not 1. Screenshot options can also capture a full page or a specified clip instead of only the visible viewport.
Start by writing down the target: do you want a 1280 CSS-pixel-wide viewport, or an output file exactly 1280 pixels wide? For a direct one-to-one bitmap at 1280 pixels, use a 1280-pixel viewport and deviceScaleFactor: 1, then avoid resizing the output.
2. Set the viewport before navigation
Set the intended dimensions explicitly before calling page.goto(). Puppeteer recommends this order because changing dimensions after loading can cause some sites to behave unexpectedly. Some combinations of mobile emulation settings can also trigger a page reload when the viewport changes.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({
width: 1280,
height: 800,
deviceScaleFactor: 1,
});
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 30000,
});
console.log('Configured viewport:', page.viewport());
console.log('Runtime measurements:', await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
documentWidth: document.documentElement.scrollWidth,
documentClientWidth: document.documentElement.clientWidth,
bodyWidth: document.body?.scrollWidth,
bodyClientWidth: document.body?.clientWidth,
})));
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
Replace the example URL and target dimensions with your own. page.viewport() reports the settings configured through Puppeteer; it does not inspect the page’s actual viewport. Use window.innerWidth and window.devicePixelRatio in page context to check runtime values.
If you use page.emulate(device), remember that device emulation applies a user agent and viewport. Do it before navigation as well, and check whether the preset’s dimensions and scale factor match your target.
3. Check the capture region
For a screenshot of the visible viewport, leave fullPage unset or set it to false. Remove any unintended clip. A clip specifies the captured region and can make output dimensions differ from the viewport; captureBeyondViewport controls whether capture may extend beyond the viewport.
| Goal | Settings to inspect | What to expect |
|---|---|---|
| Visible viewport | Omit fullPage or set false; remove an accidental clip |
Capture the current visible area |
| Entire document | Set fullPage: true |
Capture the full page area, not just the viewport |
| Specific region | Set clip deliberately; review captureBeyondViewport |
Capture the selected region, which can extend beyond the visible area |
Isolate these settings by first taking a plain viewport screenshot with no clip and no full-page capture. Add the desired option back only after the output width matches expectations.
4. Measure horizontal overflow in the page
The document’s rendered layout determines whether content extends horizontally. Compare scrollWidth, which includes content outside the visible area, with clientWidth, the element’s inner visible width. A larger scroll width indicates overflow.
const widths = await page.evaluate(() => {
const root = document.documentElement;
const body = document.body;
return {
viewport: window.innerWidth,
rootScrollWidth: root.scrollWidth,
rootClientWidth: root.clientWidth,
bodyScrollWidth: body?.scrollWidth ?? null,
bodyClientWidth: body?.clientWidth ?? null,
};
});
console.log(widths);
Check both the root element and body; page structures vary. Pseudo-elements can contribute to an element’s scrollWidth, so a DOM element scan may not identify every source by itself.
To find likely elements whose boxes extend beyond the viewport, enumerate rendered elements and inspect their bounding rectangles:
const candidates = await page.evaluate(() => {
const width = document.documentElement.clientWidth;
return [...document.querySelectorAll('body *')]
.map((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
tag: element.tagName.toLowerCase(),
id: element.id,
className: typeof element.className === 'string' ? element.className : '',
left: Math.round(rect.left),
right: Math.round(rect.right),
width: Math.round(rect.width),
position: style.position,
overflowX: style.overflowX,
};
})
.filter((item) => item.width > 0 && (item.left < 0 || item.right > width))
.sort((a, b) => b.right - a.right);
});
console.table(candidates);
Treat the list as a set of leads, not a definitive diagnosis: intentionally positioned elements may extend outside the viewport, and pseudo-elements can overflow without appearing as separate DOM nodes. Inspect the relevant CSS and ancestors in the browser as well.
5. Fix the layout cause
Once you have a candidate, inspect its width, positioning, padding, margins, and content at the target viewport. Common patterns to check include:
- Fixed-width components that exceed the available space.
- Long unbroken text or URLs that cannot wrap.
- Tables or images wider than their containers.
- Absolutely positioned elements placed beyond the viewport edge.
width: 100vwcombined with extra padding or margins. Depending on the layout, viewport width can include space that causes the element to exceed its container.
Correct the component’s responsive sizing or content handling. For example, a table may need a constrained wrapper with intentional horizontal scrolling; an image may need a maximum width; and long text may need a wrapping rule. Choose the fix based on the element’s purpose and surrounding layout.
Avoid starting with a global overflow-x: hidden rule. It can hide the visible symptom while clipping useful content and leaving the layout defect in place.
6. Capture again and compare measurements
- Keep the same URL, viewport, device scale factor, and screenshot options.
- Record
window.innerWidth,window.devicePixelRatio, rootscrollWidth, and rootclientWidth. - Save a viewport-only screenshot and inspect its bitmap dimensions with your image viewer or image-processing tool.
- Change one setting or layout rule at a time, then repeat the capture.
If document scrollWidth exceeds clientWidth, continue investigating layout overflow. If the runtime viewport is correct and there is no document overflow but the bitmap width is unexpected, check the device scale factor and capture-region options.
7. Troubleshooting
| Symptom | Likely cause to investigate | Next step |
|---|---|---|
| Saved image is wider than the configured viewport | Device scale factor, a clip region, or full-page capture | Check runtime devicePixelRatio; remove clip; capture with fullPage: false |
page.viewport() looks right, but the site renders at another width |
Configured settings differ from actual runtime dimensions, or the page uses device emulation | Measure window.innerWidth and window.devicePixelRatio after navigation |
| Document width exceeds the viewport | Rendered content extends beyond the client area | Compare root and body widths, then inspect overflowing element bounds and CSS |
| Only the full-page screenshot appears unexpectedly wide | fullPage: true captures the page-length area and the document may also overflow horizontally |
Take a viewport-only capture to separate capture mode from layout width |
| No DOM candidate explains the excess width | A pseudo-element or ancestor styling may contribute to overflow | Inspect computed styles and pseudo-elements on likely containers |
| Page changes layout after viewport setup | Viewport was changed after navigation or emulation settings caused a resize/reload | Set the viewport or emulate the device before navigation, then measure after load |
8. Performance, reliability, and cost
Runtime measurement with page.evaluate() is a small diagnostic step compared with loading and rendering a page. For repeatable captures, hold the browser version, viewport, scale factor, URL state, wait condition, and screenshot options constant. Pages can still vary with loaded content, fonts, animation, network timing, and responsive behavior, so use a consistent readiness condition and wait for any specific element that affects the layout.
Full-page captures can involve more content than viewport captures, especially on long pages. Use the capture mode that matches the task, and avoid repeating expensive navigations while tuning a CSS issue when a local reproduction is available. Puppeteer itself does not charge per screenshot; infrastructure, browser execution, and any external service costs depend on how you run captures.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. For a viewport-sized screenshot, provide the target URL and set the capture options for the dimensions you need; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.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);
Cookie and consent banners are accepted like a visitor and removed before capture, along with known newsletter popups and chat widgets; each of these steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots, and every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Does page.viewport() report the page’s actual width?
No. It reports the viewport settings configured through Puppeteer. Read window.innerWidth in the page to inspect the runtime viewport.
Should I use fullPage: true to fix a wide screenshot?
No. It captures the full page area. Use a viewport-only capture to diagnose the visible viewport, then check document overflow separately.
Can I always fix horizontal overflow with overflow-x: hidden?
That may conceal clipping without fixing its cause. Identify the overflowing content and choose a layout fix that preserves the intended behavior.


