ScreenshotNeo

BlogHow-to

How to Wait for Images and Fonts Before a Cypress Screenshot

Make Cypress screenshots reliable by waiting for the intended UI, checking important images, and awaiting web fonts before capture.

By the ScreenshotNeo team4 October 20268 min read

Before calling cy.screenshot(), wait for the application to show the state you intend to capture, verify that screenshot-critical images loaded successfully, and await document.fonts.ready. Cypress screenshots are asynchronous; the command does not wait for your application data, images, or fonts to finish loading.

The pattern below uses Cypress queries and assertions so Cypress can retry readiness checks, then returns the browser’s font readiness promise from .then() so the test waits for it.

cy.contains('h1', 'Ready').should('be.visible')

// Include only images that must appear in this screenshot.
cy.get('[data-screenshot-image]').should(($imgs) => {
  [...$imgs].forEach((img) => {
    expect(img.naturalWidth).to.be.greaterThan(0)
  })
})

cy.document().then((doc) => doc.fonts.ready)
cy.screenshot('stable-page')

1. Decide what “ready” means for this screenshot

Asset readiness is only one part of a stable capture. First identify a user-visible condition that means the relevant application state has rendered: a heading, a completed results panel, or a loading indicator disappearing. Assert that condition before checking images and fonts. This prevents a successful asset check from capturing a page whose data or client-side rendering is still incomplete.

Cypress recommends taking a snapshot after confirming the page is done changing. Screenshot options and their animation handling do not replace explicit checks for application state and assets. See the [Cypress visual-testing guidance](https://docs.cypress.io/app/guides/visual-testing) and [Cypress screenshot documentation](https://docs.cypress.io/api/commands/screenshot).

2. Check that important images actually loaded

Use naturalWidth > 0 for each relevant <img>. The complete property alone is not enough: it can be true for an image that is broken or has no source. A positive natural width is a useful assertion that the browser has image data to render.

Mark capture-critical images in application markup so the test can target them explicitly:

<img data-screenshot-image src="/assets/report-chart.png" alt="Report chart">

The Cypress query in the recommended pattern retries until the matching elements satisfy the assertion or the command times out. Keep the selector limited to assets the screenshot needs; checking every image on a large page can wait on unrelated content.

Pages where images may be absent

cy.get() expects at least one match. If zero images is a valid state, make the expected count explicit and branch accordingly:

cy.get('[data-screenshot-image]').should(($imgs) => {
  // Zero is allowed here; when present, every marked image must be usable.
  [...$imgs].forEach((img) => {
    expect(img.naturalWidth).to.be.greaterThan(0)
  })
})

This still requires the selector to match. For a genuinely optional group, first assert the container or application state that determines whether images should exist, then use a query strategy appropriate to that state. Do not silently treat a missing required image as ready.

Lazy-loaded images

A lazy image outside the viewport may not have been requested yet. If it belongs in the capture, cause the application to request it first—for example, scroll the relevant region or image into view—then run the image assertion. For a full-page capture, account for the page’s lazy-loading behavior and ensure the capture-critical images have been requested. Avoid forcing every offscreen image to load when the screenshot only covers a smaller region.

3. Wait for web fonts

document.fonts.ready resolves when the document has completed loading fonts needed for its current layout, layout work is complete, and no further font loads are needed for that state. Cypress can wait for this browser promise by returning it from a .then() callback:

cy.document().then((doc) => {
  return doc.fonts.ready
})

Place this after the page has reached the state whose fonts you want to capture, since later content can trigger additional font use. The browser API does not prove that the intended font file loaded: a fallback font can render while a webfont request has failed. If a specific typeface matters, confirm that its stylesheet and font files are available in the test environment and inspect failed requests. See [MDN’s FontFaceSet.ready reference](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready).

4. Put the checks together

This runnable test shows the full sequence. It assumes the application exposes a visible ready heading and marks the images required for the capture.

describe('stable screenshot', () => {
  it('waits for the page, its images, and fonts', () => {
    cy.visit('/report')

    cy.contains('h1', 'Ready').should('be.visible')

    cy.get('[data-screenshot-image]').should(($imgs) => {
      expect($imgs.length).to.be.greaterThan(0)
      [...$imgs].forEach((img) => {
        expect(img.naturalWidth, img.currentSrc || img.src)
          .to.be.greaterThan(0)
      })
    })

    cy.document().then((doc) => doc.fonts.ready)
    cy.screenshot('stable-report')
  })
})

Adjust the heading, route, and selector to match the application. If no marked image is required, remove the positive-count assertion or use a separate optional-image path; keep a positive-count assertion when the test is meant to guarantee that expected images exist.

5. Handle image types beyond <img>

The image-element check covers HTML image elements, not every visual asset a page can display. CSS background images, canvas drawings, video frames, and charts rendered asynchronously need their own readiness signals. Prefer an application-owned condition, such as a chart-rendered marker or a promise exposed by the component, and assert it before capture. For video, define which frame or playback state is expected rather than assuming that an image check covers it.

6. Keep the page visually stable

  • Disable or finish relevant animations. Cypress’s waitForAnimations and animationDistanceThreshold options apply to action commands; they do not globally stop all page motion before a screenshot.
  • Wait for data and rendering. A loaded image and ready font set do not prove that asynchronous application updates have finished.
  • Keep capture environments consistent. Operating system, browser, device scale, and installed fonts can change rendered pixels in visual comparisons.
  • Use a deliberate timeout. If assets have a known slower path, configure an appropriate Cypress command timeout for the relevant query rather than adding an arbitrary fixed sleep.

Cypress documents screenshot capture as asynchronous, so the page can change between issuing the command and the actual capture. Put assertions and readiness waits before cy.screenshot(); screenshot callbacks can adjust the DOM synchronously, but are not substitutes for asset readiness.

7. Troubleshoot common failures

Symptom Likely cause Fix
The screenshot contains a broken or blank image. The test checked complete, or did not check the image at all. Assert naturalWidth > 0 on the relevant image and report its currentSrc or src in the assertion message.
The image query times out. The selector matches no element, the image is still loading, its request failed, or it is lazy and has not been requested. Confirm the selector and expected count; scroll required lazy content into view; inspect the image URL and network failure.
The screenshot uses a fallback font. document.fonts.ready settled after a font failure, or the font CSS/file is unavailable in the test environment. Check stylesheet and font requests, ensure styles are included, and verify the specific font is available before capture.
The page still changes after the checks. Data rendering, transitions, animation, or another asynchronous component was not covered by the readiness conditions. Add an assertion for the intended UI state and a component-specific ready signal; disable or finish relevant animations in the test setup.
A chart or background image is missing although all image assertions pass. Those visuals are not necessarily represented by an <img>. Wait for the chart/component render signal or check the relevant resource and application state explicitly.
Visual diffs vary between runs or machines. Browser, operating system, scaling, or installed fonts differ. Run comparisons in a consistent rendering environment with the same viewport and device scale.

8. Performance and reliability notes

Retryable assertions wait only as long as needed for their conditions, unlike a fixed sleep that always consumes its full duration and may still be too short. Narrow selectors also keep the test from waiting on irrelevant images. Waiting for document.fonts.ready is tied to the current document font set; it does not require an arbitrary delay.

For reliability, make each readiness condition observable and specific: one for application state, one for required images, and one for fonts. If an image or font request fails, fail the test with enough context to diagnose the resource instead of saving a misleading screenshot. Cypress screenshot capture itself has asynchronous timing; do not treat a particular capture duration as a guarantee.

Or skip the browser setup

For a captured website page outside a Cypress test, [ScreenshotNeo](https://screenshotneo.com) provides a screenshot API and MCP server. One GET request can return an image or PDF, and its options include full-page capture with lazy images loaded, waiting for a selector, a delay or network idle, and custom CSS or JavaScript. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for parameters and configuration.

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 = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
  • Cookie banners are accepted and removed, and newsletter popups and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers identify the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.

FAQ

Does waiting for fonts also wait for images?

No. document.fonts.ready concerns fonts and layout; check required images separately.

Is img.complete enough?

No. It can be true for a broken image. Assert that a required image has a positive naturalWidth.

Will these checks make screenshots pixel-identical across machines?

No. They address readiness, while browser, operating system, scaling, and installed fonts can still affect pixels.