How to Fix Playwright Screenshots When the Mobile Viewport Meta Tag Is Missing
Add a viewport meta tag and enable Playwright mobile emulation. Learn how to diagnose layout, capture-size, and resolution issues.
Direct fix: Add <meta name="viewport" content="width=device-width"> inside the page’s <head>, then capture with Playwright mobile emulation enabled. A narrow viewport alone does not enable all mobile behavior: check isMobile as well as the viewport dimensions.
Set the viewport and mobile settings on the browser context before navigating when possible. If the result still looks wrong, check that the expected document contains the tag, and distinguish a responsive-layout problem from a full-page capture or image-scale setting.
1. Add the viewport declaration
Put this in the HTML document’s <head>:
<meta name="viewport" content="width=device-width">
The declaration asks the browser to use the device width as the layout viewport. Without it, some mobile browsers can lay out a page in a wider virtual viewport and scale the result down to fit. Text and controls may look tiny, and narrow-screen media queries may not behave as expected.
initial-scale=1 is commonly unnecessary. Add it only if you have a specific initial scaling problem; investigate horizontal overflow directly rather than using zoom settings to conceal it. Do not add user-scalable=no to stabilize a screenshot. Users should retain the ability to zoom.
2. Configure Playwright for mobile behavior
Here is a complete runnable JavaScript example using Playwright Test. It sets the context configuration before navigation, visits a page that includes the viewport declaration, and saves a viewport screenshot.
import { test } from '@playwright/test';
test('capture the responsive mobile layout', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
screen: { width: 390, height: 844 },
deviceScaleFactor: 1,
isMobile: true,
hasTouch: true,
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'mobile.png' });
await context.close();
});
Replace https://example.com with your route. The target page needs the viewport declaration in its own rendered document. The settings above make the context mobile-aware and choose a 390 × 844 CSS-pixel viewport; use dimensions appropriate to the case you are investigating.
Alternatively, use a built-in device descriptor and then override the fields you need. Put overrides after the spread so they take precedence:
import { chromium, devices } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({
...devices['iPhone 13'],
viewport: { width: 390, height: 844 },
isMobile: true,
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'mobile.png' });
await browser.close();
Device descriptor names are Playwright-provided configuration; choose one available in the Playwright version installed in your project. Explicit settings are useful when you want a controlled viewport rather than a particular device profile.
3. Check the document and emulation settings
- Confirm the tag is in the loaded page. Check the actual route and rendered document, not only a template or a different page in the site. In a client-rendered app, verify the final document’s head after navigation.
- Confirm mobile emulation is enabled. Playwright’s
isMobilesetting determines whether the meta viewport is taken into account and enables touch behavior. A smallviewportsetting by itself is not the same as settingisMobile. - Choose viewport dimensions deliberately.
viewportsets the page viewport.screencan be configured separately when you need deliberate control over screen dimensions too. Playwright notes that changing the viewport after navigation can affect layout and resets screen size, so prefer context settings before creating or navigating the page. - Keep capture area separate from layout.
fullPage: truecaptures the full scrollable page; the default capture is the visible viewport. - Keep output scale separate from layout. Screenshot
scalecontrols output pixel density.devicescale can produce more output pixels thancssscale under high-DPI emulation. Neither option fixes a desktop-like responsive layout.
Example of a full-page capture at CSS scale:
await page.screenshot({
path: 'mobile-full-page.png',
fullPage: true,
scale: 'css',
});
Use a viewport-only screenshot when diagnosing the initial mobile layout, then capture full-page output if that is the behavior you need. Compare captures with the browser engine, device descriptor, isMobile, viewport width and height, device scale factor, navigation state, fullPage, and screenshot scale held constant. That makes it easier to tell whether a difference comes from responsive layout, capture area, or output resolution.
4. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Page looks like a tiny desktop page | The loaded document lacks the viewport declaration, or mobile emulation is off. | Add width=device-width to the loaded document’s head; set isMobile: true and confirm the target viewport. |
| Changing viewport width has no visible effect | The page may be using a wide layout viewport because the meta tag is absent, or the tested route differs from the edited template. | Inspect the final route’s document head and verify the browser context settings before navigation. |
| Media queries still select a desktop layout | The page may not be using the expected layout viewport, or the chosen width is not below the site’s breakpoint. | Confirm the meta tag and isMobile, then check the site’s responsive breakpoints and actual viewport width. |
| Screenshot dimensions are larger than expected | fullPage captures the scrollable page, or device-scale output creates more pixels than CSS scale. |
Use viewport capture for the visible screen and set scale: 'css' when CSS-pixel output dimensions are desired. |
| Layout changes after resizing during the test | Viewport changes after navigation can affect layout and reset the screen size. | Set viewport and screen on the context before navigation; if resizing is necessary, do it deliberately and wait for the page to settle before capture. |
| A device descriptor override appears ignored | The descriptor spread came after the explicit setting and overwrote it. | Spread the descriptor first, then put your viewport, isMobile, or other overrides after it. |
5. Performance, reliability, and cost
The viewport fix is a small document change; screenshot reliability mostly depends on making the capture environment repeatable. Keep the browser engine, context settings, route, and capture options fixed when comparing results. Set the context before navigation, use the same page-ready condition for each run, and avoid changing viewport settings midway through a capture workflow unless resizing itself is under test.
For CI, make the viewport and device scale explicit so output dimensions do not silently vary with a different device profile. Use viewport-only capture for fast layout checks and full-page capture only when the complete document is needed. Higher pixel density and full-page images produce larger output files and can take longer to process. The source material provides no benchmark or fixed cost figure for Playwright screenshot runs; actual resource use depends on the page, browser, and capture settings.
6. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request takes a URL and returns an image or PDF; see the API documentation. For example, this cURL request saves a WebP capture:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python:
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)
Node.js:
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);
These calls capture a URL; they do not change that page’s HTML. If you maintain the page, add the viewport declaration and test it with Playwright as described above. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are never billed. Its MCP server lets AI agents use screenshot tools. 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.
7. FAQ
Does setting a 390-pixel viewport automatically make the page mobile?
No. Configure mobile emulation with isMobile as well as setting the viewport dimensions.
Should I always include initial-scale=1?
Usually the core fix is width=device-width. Add an initial scale only for a specific scaling issue, and fix horizontal overflow at its source.
Will fullPage make the responsive layout correct?
No. It changes the captured area. Fix the page’s viewport declaration and context emulation to address responsive layout.
Can I disable pinch zoom for stable screenshots?
Do not use user-scalable=no for this. It restricts user zoom and does not solve a missing viewport declaration.


