Headless browsers usually do not ignore viewport sizes in matchMedia(). The apparent mismatch normally comes from automation settings: a framework viewport is separate from the operating-system window, defaults may be unexpected, viewport: null can make dimensions depend on the host window, or the query may test media type or a preference rather than width. Configure a deterministic CSS viewport before navigation, then log the values the page actually sees.
What matchMedia() is actually measuring
window.matchMedia('(min-width: 768px)').matches evaluates a media condition inside the page. Width and height features refer to the page’s CSS viewport, not necessarily the physical monitor, an operating-system window, or a device’s screen resolution. Puppeteer’s viewport API likewise expresses dimensions in CSS pixels; those dimensions should not be treated as interchangeable with physical display pixels or window.screen values. See the Puppeteer Viewport interface.
Headless mode by itself does not switch matchMedia() off. If the page reports a result that conflicts with your expectation, first determine which viewport the automation library created, which browser engine and channel are running, and what the query tests.
Why the result differs from your expected viewport
The framework has its own viewport defaults
Browser automation creates a browser context or page with settings independent of your desktop window. Playwright documents a default context viewport of 1280×720. A test that assumes the host display size, a maximized window, or a mobile device width will therefore evaluate against a different CSS viewport unless you set one explicitly. The default and the behavior of viewport: null are documented in Playwright’s Browser API.
#1 Best Overall
viewport: null follows the host window
In Playwright, viewport: null disables the consistent emulated viewport. The page size then depends on the host window, and Playwright warns that this makes test execution nondeterministic. CI runners, developer laptops, virtual displays and headed sessions can all provide different host dimensions. Use null only when that variability is intentional.
You resize after the page has already made decisions
Responsive code can run during initial parsing, hydration or a route transition. Set the desired size in the context or page before navigation. Playwright’s page.setViewportSize() API resizes the page and also resets the screen size; its documented examples are in the Page API.
The query is not a viewport-width query
Queries such as (prefers-color-scheme: dark), (prefers-reduced-motion: reduce), (orientation: landscape), (media: print) or color and contrast features do not become true merely because you changed width. Playwright separates resizing from media emulation. Use page.emulateMedia() for media type and documented preference settings; use viewport APIs for width and height. The distinction is described in Playwright’s Emulation guide.
Headless Chromium implementations can differ
Playwright documents that its default Chromium headless operation uses a separate headless shell. New headless mode can be selected through the chromium channel, and behavior can differ in some cases. A headed run and a headless run are not equivalent merely because both request the same width and height. Record the browser channel and version when investigating a discrepancy, then compare the exact configurations.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsRank #2
Make a Playwright viewport test deterministic
The safest pattern is to choose the viewport at context creation, before opening and navigating the page.
JavaScript example
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const result = await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
media: window.matchMedia('(max-width: 600px)').matches,
}));
console.log(result);
await browser.close();
Replace the URL and query with your test case. The important ordering is viewport, context, page, navigation, then evaluation. For a desktop check, create another context with its own explicit dimensions rather than relying on a resized host window.
Resizing an existing page
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
console.log(await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
matches: window.matchMedia('(min-width: 1024px)').matches,
})));
When a site chooses a layout only during its first load, navigate after resizing. If application code listens for resize events correctly, changing the viewport after navigation can be useful for breakpoint transitions; recreating the page is more reliable for initial-load behavior.
Preference and media-type emulation
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
colorScheme: 'dark',
reducedMotion: 'reduce',
});
const page = await context.newPage();
await page.emulateMedia({ media: 'print' });
await page.goto('https://example.com');
Keep these controls separate in your test description: viewport dimensions, media type and user preferences are different inputs even when the page reads all of them through matchMedia().
Free tools Windows power users keep installed
One-click scans. No signup required.
A diagnostic sequence that finds the mismatch
- Identify the stack. Record the automation library and version, browser engine and version, launch channel, operating mode (headed or headless), and the exact media query.
- Set dimensions explicitly. Configure context or page width and height before navigation. Avoid an assumed desktop window size and avoid
viewport: nullwhen reproducibility matters. - Log page-observed values. Run this in the page, not in the Node.js process:
await page.evaluate((query) => ({ query, innerWidth: window.innerWidth, innerHeight: window.innerHeight, screenWidth: window.screen.width, screenHeight: window.screen.height, matches: window.matchMedia(query).matches, }), '(min-width: 768px)'); - Classify the query. Width and height require viewport control.
print,screen, color scheme and reduced motion require the corresponding media or preference emulation. A query can also contain several conditions; log each condition separately while debugging. - Compare equivalent runs. Hold CSS width and height constant while changing only one axis at a time: framework version, browser engine, headless/headed mode, Chromium channel, or media emulation. This prevents a host-window difference from being mistaken for a browser defect.
- Check timing. Log before navigation, after DOM content loads and after hydration or route changes. If the value changes, inspect resize listeners and application state rather than assuming
matchMedia()is stale.
Headless versus headed: a controlled comparison
Run the same test twice with the same explicit context viewport. For Chromium, compare the normal headless path with the documented new headless channel when your Playwright version supports it, and then run headed mode. Keep the browser executable and version visible in CI logs. If only one combination differs, the discrepancy is associated with that implementation path; it is not evidence that all headless browsers ignore viewport sizes.
Do not “fix” a failing test by reading window.screen.width and substituting it for window.innerWidth. Screen dimensions answer a different question. Assert the value your responsive code uses, usually the CSS viewport, and separately record screen values when diagnosing emulation.
Common failure modes and fixes
Always seeing 1280 pixels
Cause: Playwright’s documented default context viewport is active. Fix: pass viewport: { width, height } to browser.newContext(), or call setViewportSize() before navigation.
Different results on a laptop and CI
Cause: viewport: null or another host-window-dependent setup. Fix: use fixed CSS dimensions and record browser and channel versions. This removes the host display from the test’s inputs.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Width changes but dark mode does not
Cause: color scheme is a preference, not a width condition. Fix: set colorScheme in the context or use the documented media-emulation API.
Print rules never activate
Cause: resizing does not change the media type. Fix: call page.emulateMedia({ media: 'print' }) and then evaluate the query.
Headless and headed disagree at the same size
Cause: different Chromium headless implementations, browser channels, versions or launch flags. Fix: capture the full launch configuration and compare the exact headless path with headed mode, as Playwright recommends in its Browsers documentation.
The assertion is correct, but the screenshot looks wrong
Cause: screenshot capture can occur before fonts, lazy content or application hydration settle, or the screenshot tool can use a different viewport than the assertion. Fix: use one configured context, wait for the relevant selector or network state, and save the logged dimensions beside the image.
Best Value
Performance, reliability and test design
- Create contexts with the target viewport up front; this avoids unnecessary resize cycles and makes parallel desktop/mobile projects independent.
- Use a small matrix of representative CSS widths, then add a case for every breakpoint your application actually defines. Do not infer breakpoints from monitor resolutions.
- Keep browser engine, channel and version pinned in continuous integration when pixel-level or responsive assertions matter.
- Wait for the condition your application needs: a selector, a known load state or a bounded delay. “Network idle” can be unsuitable for pages with persistent analytics or sockets.
- Persist diagnostic output containing query, viewport, screen values, media type, preferences, browser and mode. It turns a vague mismatch into a reproducible configuration.
Or skip the browser setup
If your goal is a clean page image or PDF rather than debugging a browser test, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP or PDF; its capture options include viewport and device presets, full-page lazy-image loading, selectors, dark mode, custom CSS and JavaScript, waits, request blocking, cookies, headers, geolocation, resizing and PDF controls.
One request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for parameter details. The equivalent Python and Node.js calls are:
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}`);
Before capture, ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Key takeaways for a failing test
matchMedia()evaluates page media conditions; headless mode does not inherently disable viewport matching.- Set an explicit CSS viewport before navigation and distinguish it from
screendimensions. - Use media emulation for print and preferences, not viewport resizing.
- Treat
viewport: nullas host-window-dependent and potentially nondeterministic. - Record browser channel and compare the exact headed and headless implementations when behavior differs.
Frequently Asked Questions
Does increasing the host machine’s monitor resolution change a Playwright viewport?
Not when the context has an explicit viewport. A host-window size matters when you opt out with settings such as Playwright’s viewport: null.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Should responsive assertions use window.innerWidth or window.screen.width?
Use the value that corresponds to the behavior under test. CSS breakpoint logic normally follows the viewport, while screen describes emulated display dimensions and is a separate diagnostic value.
Can the same CSS viewport produce different results across Chromium runs?
Yes. Browser version, launch channel and the selected headless implementation can differ, so pin and record those inputs when comparing runs.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

