ScreenshotNeo

BlogHow-to

Fix Puppeteer Screenshots That Ignore the Mobile Viewport

Set Puppeteer’s mobile viewport before navigation, then check device emulation, page measurements and screenshot options when the result still looks desktop-sized.

By the ScreenshotNeo team4 October 20268 min read

If a Puppeteer screenshot shows the desktop layout, configure the page viewport or emulate a device before navigating, then check what the page actually reports as its width. A screenshot’s crop options affect the captured region; they do not make a page use a mobile CSS layout.

For a specific responsive breakpoint, use page.setViewport(). If the test also needs a device user agent and bundled device metrics, use page.emulate(). After navigation, compare Puppeteer’s configured viewport with window.innerWidth and document.documentElement.clientWidth inside the page.

1. Set a mobile-sized viewport before navigation

This runnable example uses a 390 × 844 CSS-pixel viewport as an illustrative size. It is not a universal phone standard. Replace the target URL and dimensions with the ones your test needs.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();

    await page.setViewport({
      width: 390,
      height: 844,
      deviceScaleFactor: 1,
    });

    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
    });

    console.log('Configured viewport:', page.viewport());
    console.log('Page measurements:', await page.evaluate(() => ({
      innerWidth: window.innerWidth,
      clientWidth: document.documentElement.clientWidth,
      innerHeight: window.innerHeight,
      userAgent: navigator.userAgent,
    })));

    await page.screenshot({ path: 'mobile.png' });
  } finally {
    await browser.close();
  }
})();

page.setViewport() configures dimensions and optional properties such as device scale factor. It does not, by itself, set a phone user agent. Puppeteer advises setting the viewport before navigation because some sites do not expect phones to change size; changing mobile or touch properties can reload a page in some cases. See the official Page.setViewport() documentation.

2. Emulate a named device when the test needs its profile

Device emulation applies a known device profile, including its viewport and user agent. Use it before navigation so the site sees the emulated configuration during its initial load.

const puppeteer = require('puppeteer');
const { KnownDevices } = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.emulate(KnownDevices['iPhone 17 Pro']);

    await page.goto('https://example.com', {
      waitUntil: 'networkidle2',
    });

    console.log('Configured viewport:', page.viewport());
    console.log('Actual layout width:', await page.evaluate(() => ({
      innerWidth: window.innerWidth,
      clientWidth: document.documentElement.clientWidth,
      userAgent: navigator.userAgent,
    })));

    await page.screenshot({ path: 'device.png' });
  } finally {
    await browser.close();
  }
})();

Device names and profile details can change with Puppeteer releases. Check the Page.emulate() documentation and the KnownDevices exported by the Puppeteer version installed in your project. Avoid copying an old device name without checking that it exists in your version.

3. Diagnose the configured viewport and the page layout

page.viewport() tells you Puppeteer’s configured settings. It does not inspect the actual page viewport. When these values look right but the page still looks wrong, measure the page after it loads:

const diagnostics = await page.evaluate(() => ({
  innerWidth: window.innerWidth,
  clientWidth: document.documentElement.clientWidth,
  innerHeight: window.innerHeight,
  screenWidth: window.screen.width,
  devicePixelRatio: window.devicePixelRatio,
  userAgent: navigator.userAgent,
  viewportMeta: document.querySelector('meta[name="viewport"]')?.content ?? null,
}));

console.log({ configured: page.viewport(), diagnostics });
What you see What to check Next step
page.viewport() is wide A later call to setViewport(), page creation defaults, or connection options Set the intended viewport after creating the page and before navigating; search for later overrides.
Configured width is narrow, but innerWidth is wide Viewport changes after navigation, page state, or runtime setup Measure immediately before capture and inspect scripts that resize or replace the page.
innerWidth is narrow, but desktop content remains Responsive CSS breakpoints, application state, and the page’s viewport meta tag Confirm the expected mobile breakpoint and inspect whether the markup declares a suitable viewport.
The image dimensions look larger than expected deviceScaleFactor and screenshot cropping options Compare CSS-pixel layout measurements with output pixel dimensions; review fullPage and clip.

The viewport meta tag, application CSS, scripts, browser version and custom connection setup can all affect a particular result. The measurements help narrow down the cause; the configured viewport alone cannot prove what layout the page rendered.

4. Check connection defaults and headless screen settings

When using puppeteer.connect(), inspect defaultViewport and any subsequent viewport calls. Puppeteer documents a default viewport of 800 × 600 for ConnectOptions.defaultViewport. Set it explicitly when connecting if you rely on a particular page size:

const browser = await puppeteer.connect({
  browserWSEndpoint: process.env.PUPPETEER_WS_ENDPOINT,
  defaultViewport: { width: 390, height: 844 },
});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'connected-mobile.png' });
} finally {
  await browser.disconnect();
}

Use an endpoint available in your environment; this example reads it from an environment variable rather than embedding a credential. When launching a local browser, newPage() also receives the browser’s default page viewport unless you override it.

Page viewport and browser screen/window size are related but distinct. Puppeteer’s screen configuration guide describes a single 800 × 600 screen in headless mode when neither --screen-info nor --window-size is specified. If a workflow manipulates the browser window, follow the separate screen configuration and window management guides. For a normal page screenshot, start by setting the page viewport.

5. Make sure screenshot options capture the intended region

page.screenshot() captures the rendered page. Its options can change how much of that page appears in the image, but they do not set responsive breakpoints or emulate a phone.

Option Effect Use it when
fullPage: true Captures the full page height You need a tall page rather than only the current viewport.
clip Captures a specified rectangle You need a particular region; ensure its coordinates and dimensions match the page’s CSS-pixel layout.
captureBeyondViewport Controls capturing outside the current viewport You are using a clip or other capture area outside the viewport. Puppeteer documents a default of false without a clip and true when a clip is supplied.
// The visible mobile viewport
await page.screenshot({ path: 'viewport.png' });

// The entire page at the current layout width
await page.screenshot({ path: 'full-page.png', fullPage: true });

// A region in CSS pixels
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 390, height: 500 },
});

See Puppeteer’s ScreenshotOptions reference and screenshots guide. If the layout itself is wrong, return to viewport configuration and page measurements.

6. Wait for the state you intend to capture

A page can have the right viewport yet show an intermediate layout because the application has not finished rendering. Puppeteer’s screenshot guide demonstrates waiting for networkidle2, but no lifecycle event guarantees that every site’s application state, fonts, images or animations are ready.

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.mobile-navigation', { timeout: 10000 });
await page.screenshot({ path: 'ready-mobile.png' });

Replace .mobile-navigation with a selector that means the desired state is ready on your page. If the page uses lazy-loaded content, scroll the needed content into view or use a full-page workflow that triggers the relevant loading. Avoid an arbitrary long sleep when a concrete selector or state check is available.

7. Common causes and fixes

Symptom Likely cause Fix
Screenshot has desktop navigation Viewport was set after goto(), or a later call restored desktop dimensions Set or emulate before navigation. Log page.viewport() immediately before capture and inspect later calls.
Small image, but desktop layout The screenshot was cropped or scaled; the CSS viewport stayed wide Set the page viewport. Use clip only to select an area, not to simulate a phone.
Width reports 390 but site serves desktop markup Site behavior depends on user agent, viewport metadata, or application logic Use device emulation if a device user agent is needed; inspect the viewport meta tag and application breakpoint logic.
Layout changes after initial load Application scripts, hydration, delayed content, or a responsive resize handler Wait for the page-specific ready selector and recheck dimensions immediately before capture.
Mobile layout appears only on a second load Mobile/touch configuration was applied after navigation and triggered a reload or site-specific transition Apply emulation before navigation, then navigate once with the final device settings.
Only a tall or partial image is wrong fullPage, clip or captureBeyondViewport changed the captured region Use a plain screenshot to validate the viewport first, then add capture options.
Works locally but not through a remote browser Connected browser defaults or remote launch settings differ Set defaultViewport on connect and explicitly configure the page before navigation.

8. Performance, reliability and cost

  • Set the viewport once, early. Applying mobile settings before navigation avoids a second page load in cases where changing mobile or touch properties reloads the page.
  • Choose the smallest readiness condition that matches the page. Network-idle waits can be unsuitable for pages with persistent requests; a page-specific selector can make readiness clearer. Any timeout should be long enough for your environment but bounded so a broken page does not stall a job indefinitely.
  • Keep dimensions and scale intentional. A higher device scale factor can increase output pixel dimensions and memory use. Use it only when higher-density output is needed, and check the generated image size in your workload.
  • Reuse browser processes carefully. Reusing a browser can reduce repeated startup work in a service, but isolate pages and close them after jobs. Restart or recycle the browser on a policy appropriate to your workload; Puppeteer’s viewport docs do not specify a universal recycling interval.
  • Budget for failed navigations and retries. Set navigation and selector timeouts, record the URL and viewport with failures, and retry only transient errors. Unbounded retries can amplify load and cost.

These are operational considerations, not published benchmarks. The Puppeteer documentation cited here does not provide universal latency, memory or cost figures; measure your own pages, browser version and hosting environment.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF, so you do not need to launch and configure Puppeteer for a straightforward capture. See the ScreenshotNeo API documentation for the supported parameters.

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,
)
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}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
  • Cookie and consent banners are accepted, and 60+ known consent platforms, newsletter popups and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads and cache hits are not billed. Each response says which result occurred in X-Page-Verdict and X-Billed headers.
  • An MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

10. FAQ

Does setting a narrow viewport guarantee the site will use its mobile design?

No. It gives the page a narrow viewport; the site’s CSS, viewport metadata, user agent and application logic determine the rendered design.

Should I use setViewport() or emulate()?

Use setViewport() for specific dimensions. Use emulate() when a named device profile and its user agent are relevant.

Does fullPage: true emulate a phone?

No. It changes the vertical capture extent. Configure the viewport separately.

Is the 800 × 600 default the mobile viewport?

No. It is Puppeteer’s documented default for connection viewport settings and is also the default headless screen configuration in the described setup. Set the page viewport to your target dimensions explicitly.

Official references