ScreenshotNeo

BlogHow-to

How to Capture a Mobile Website Screenshot with Web Fonts Loaded in Playwright

Use Playwright’s mobile device emulation and wait for document.fonts.ready before capturing. This guide covers runnable code, capture options, troubleshooting, and alternatives.

By the ScreenshotNeo team4 October 20267 min read

To capture a mobile website screenshot with web fonts loaded in Playwright, create a browser context with a mobile device preset, navigate to the page, wait for document.fonts.ready, and then call page.screenshot(). This ensures the fonts used by the document are ready at the time of capture. It does not, by itself, wait for images, application data, animations, or later page changes.

1. Complete Playwright example

Install Playwright and its Chromium browser if they are not already installed:

npm install playwright
npx playwright install chromium

Save this as mobile-screenshot.js and run it with node mobile-screenshot.js:

const { chromium, devices } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const context = await browser.newContext({
      ...devices['iPhone 13'],
    });
    const page = await context.newPage();

    await page.goto('https://example.com', {
      waitUntil: 'load',
      timeout: 30_000,
    });

    // Wait until the document's font-loading work is complete.
    await page.evaluate(() => document.fonts.ready);

    await page.screenshot({
      path: 'mobile-page.png',
      fullPage: true,
      scale: 'css',
    });
  } finally {
    await browser.close();
  }
})();

The iPhone 13 preset is an example. Choose a preset that matches the layout scenario you need, or define a viewport and device scale factor yourself. Playwright presets configure simulated device properties such as user agent, screen size, viewport, and touch; they do not turn an emulated run into a capture from a physical phone. See [Playwright’s emulation guide](https://playwright.dev/docs/emulation).

2. Why wait for document.fonts.ready?

Web fonts may load after the initial HTML arrives. If the screenshot is taken before the page has finished its font work, text can appear in a fallback font or reflow after capture. The browser exposes document.fonts.ready as a promise for the document’s font-loading set. Playwright’s page.evaluate() waits for a returned promise to resolve, so it can be awaited directly before the screenshot. See [Playwright’s page.evaluate() documentation](https://playwright.dev/docs/api/class-page#page-evaluate).

await page.evaluate(() => document.fonts.ready);

This is a font-readiness step, not a universal page-readiness signal. If the target app loads content asynchronously, wait for an app-specific selector or condition too. If images affect the result, wait for those images separately. Avoid replacing a known readiness condition with a fixed delay: a delay can be too short on a slow run and unnecessarily long on a fast one.

3. Choose the mobile profile and output dimensions

Choice Use it when What to know
Device preset You want a repeatable simulated mobile configuration. The preset supplies device-related browser settings. Record the preset and browser engine with the result.
Custom viewport You need a specific layout width or height. Set the viewport in the context configuration; retain any other device settings your scenario needs.
fullPage: false You need only the currently visible viewport. This is the default viewport-style capture.
fullPage: true You need the page’s full scrollable content. Long pages produce taller files and may expose page behavior that differs during scrolling.
scale: 'css' You want one output pixel per CSS pixel. Useful when the desired dimensions correspond to the CSS layout size.
scale: 'device' You want device-pixel output. This is the documented default. High device scale factors can create larger images.

For example, use viewport capture and device-pixel scale by omitting fullPage and scale:

await page.screenshot({ path: 'viewport.png' });

Or make the choices explicit:

await page.screenshot({
  path: 'viewport-css-pixels.png',
  fullPage: false,
  scale: 'css',
});

Playwright documents the screenshot path, full-page option, and scale settings in its [Page API](https://playwright.dev/docs/api/class-page#page-screenshot). The css scale yields one image pixel per CSS pixel; device yields one pixel per device pixel.

4. Add checks for the rest of the page

Font readiness does not guarantee that every resource or application state is ready. Add only the checks the page requires, and prefer a meaningful condition over an arbitrary sleep.

Wait for a known app element

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('[data-testid="page-ready"]').waitFor({ state: 'visible' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'ready.png', fullPage: true, scale: 'css' });

Replace the example selector with a real readiness marker from the site. If the app changes its layout after that marker appears, wait for the more specific state that matters to your screenshot.

Wait for images when they matter

await page.evaluate(async () => {
  const images = Array.from(document.images);
  await Promise.all(images.map((image) => {
    if (image.complete) return Promise.resolve();
    return new Promise((resolve) => {
      image.addEventListener('load', resolve, { once: true });
      image.addEventListener('error', resolve, { once: true });
    });
  }));
});

This waits for images present in the document when the check runs, resolving on either load or error so a broken image does not hang the script. Lazy-loaded images may not start loading until they approach the viewport. A full-page screenshot and this check do not automatically guarantee that every lazy image has been requested; use a page-specific strategy if those images must appear.

Use network idle only when it fits

A page with polling, analytics, or persistent connections may never become idle. Network-idle waiting can also finish before a delayed UI update. Prefer a specific selector or app signal where possible. Use navigation waits as one part of readiness, then await fonts and any application-specific conditions before capture.

5. Common errors and fixes

Symptom Likely cause Fix
Text uses a fallback font The screenshot happened before the page’s font work finished, or the font request failed. Await document.fonts.ready. If the font still does not appear, inspect the page’s font requests and verify that the site can load them in this browser context.
Screenshot shows an incomplete app Navigation completed before client-side data or UI rendering finished. Wait for a stable, app-specific selector or state before waiting for fonts and capturing.
Images are missing Images were still loading, failed, or were lazy-loaded outside the initial viewport. Add an image readiness check, investigate failed image requests, and account for lazy loading if full-page content is required.
Navigation times out The page did not reach the chosen navigation condition within the timeout, or ongoing requests prevent the selected condition. Choose a navigation condition appropriate to the site, set a justified timeout, and wait for a concrete page-ready signal after navigation.
Output is larger than expected scale: 'device' uses device-pixel dimensions, or the full-page image is very tall. Use scale: 'css' for CSS-pixel dimensions, capture only the viewport, or resize the output downstream if suitable.
Layout does not match the intended phone The selected preset or viewport does not represent the layout scenario. Choose the appropriate preset or set the viewport explicitly. Treat the result as emulation and note its configuration.
Run hangs while waiting for idle The page keeps network activity open, such as polling or streaming. Use a specific readiness selector or application condition instead of waiting indefinitely for network quiet.

6. Reliability, repeatability, and performance

  • Keep the setup explicit: use a recorded device profile, browser engine, viewport, screenshot scale, and full-page setting when comparing captures.
  • Wait for the condition you need: font readiness addresses fonts. Add separate checks for app data, images, or UI transitions only when they affect the image.
  • Use bounded waits: set navigation and readiness timeouts so a failed page does not stall a batch indefinitely. Handle failures at the call site and save diagnostics appropriate to your workflow.
  • Manage browser lifetime: close the browser in a finally block, as in the example, so an error during capture does not leave the browser process running.
  • Keep output size in mind: full-page and device-scale captures can create larger files. CSS scale or viewport capture can reduce output dimensions when that suits the task.
  • Do not assume pixel identity across environments: the selected profile and capture options improve repeatability, but the cited Playwright documentation does not promise identical pixels across operating systems, browser versions, or font environments.

7. Or skip the browser setup

If you need a screenshot rather than a locally managed browser, [ScreenshotNeo](https://screenshotneo.com) provides a screenshot API. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request options. This one-call example saves a WebP response:

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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses 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 per month with no card; paid plans start at $5 for 3,000 screenshots. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/).

8. FAQ

Does document.fonts.ready prove every font rendered correctly?

It indicates that the document’s font-loading set is ready. It does not prove a particular font URL succeeded or that the page uses the intended face; check the page’s font requests and styling if the result looks wrong.

Should I use a mobile preset or set a viewport?

Use a preset when its simulated device configuration fits your scenario. Set or override the viewport when you need specific dimensions. Record the configuration so captures can be interpreted later.

Does a mobile Playwright screenshot come from a real phone?

No. A device preset configures emulated browser properties. Describe it as a mobile emulation capture and identify the preset or viewport.

Will fullPage: true load every lazy image?

Do not assume so. Full-page capture controls the screenshot extent; lazy-loading behavior depends on the page. Add a page-specific loading strategy if offscreen images are required.