Playwright Screenshot Has Missing SVG Icons: Font and Asset Loading Fixes
Diagnose missing icons in Playwright screenshots by tracing SVG and font requests, checking browser errors, and waiting for the page’s actual ready state.
When SVG icons disappear from a Playwright screenshot, first identify how each icon is rendered, then inspect the browser’s network and console output for failed or blocked assets. If the icon is a font glyph, wait for document.fonts.ready after confirming the font loaded. Finally, assert the page’s real ready state before capturing. A longer timeout alone will not repair a bad URL, a failed font, or an SVG that depends on resources unavailable in image context.
This guide covers the four common rendering paths—inline SVG, SVG in an <img>, CSS images or masks, and icon-font glyphs—and gives runnable Playwright examples for each diagnosis and fix.
1. Identify how the icon is rendered
Inspect the element in the DOM and check its computed styles. The word “SVG” describes a file format, not necessarily the browser mechanism: an inline <svg> is part of the document, while an SVG loaded through an image or CSS URL follows image-resource rules. An icon that looks like SVG may instead be a character from an icon font.
| Rendering path | What to inspect | Typical failure |
|---|---|---|
Inline <svg> |
SVG markup, viewBox, fill/stroke, computed color, visibility, clipping |
Zero-sized or hidden element, inherited color matching the background, missing referenced definition |
<img src="…svg"> |
src, final URL, response status and content type, intrinsic and CSS dimensions |
Wrong path, failed response, or external dependency unavailable in image context |
| CSS background or mask | Computed background-image/mask-image, stylesheet URL base, pseudo-elements |
Relative URL resolves from a different stylesheet directory than expected |
| Icon-font glyph | Font-family, glyph content, font request, font loading and decoding errors | Font did not load, preload mode mismatch, or fallback font lacks the glyph |
MDN documents SVG use in image and SVG elements as well as CSS image contexts in its SVG image guide. Inline SVG is usually the right fit when the parent page must style paths directly. An external image is convenient for a self-contained asset. CSS backgrounds and masks suit decoration, but need particular care with stylesheet-relative URLs.
// In the browser console or a Playwright evaluate call:
const icon = document.querySelector('[data-testid="save-icon"]');
console.log({
tag: icon?.tagName,
src: icon?.getAttribute('src'),
outerHTML: icon?.outerHTML,
backgroundImage: icon && getComputedStyle(icon).backgroundImage,
maskImage: icon && getComputedStyle(icon).maskImage,
fontFamily: icon && getComputedStyle(icon).fontFamily,
color: icon && getComputedStyle(icon).color,
display: icon && getComputedStyle(icon).display,
visibility: icon && getComputedStyle(icon).visibility,
box: icon && icon.getBoundingClientRect().toJSON()
});
2. Read the network and console evidence
Before adding a wait, check whether the expected asset was requested and what happened. In Playwright UI Mode, review the console and network log around the capture. Look for a missing request, 404, unexpected redirect or final URL, wrong content type, blocked cross-origin request, failed font decode, or browser console error. A page that opens at a different route than expected can make a relative asset path resolve somewhere else.
For a reproducible trace in a test, collect failed requests and page errors:
import { test, expect } from '@playwright/test';
test('diagnose icon assets', async ({ page }) => {
page.on('requestfailed', request => {
console.error('REQUEST FAILED', request.url(), request.failure()?.errorText);
});
page.on('response', response => {
if (response.status() >= 400) {
console.error('BAD RESPONSE', response.status(), response.url());
}
});
page.on('console', message => {
if (message.type() === 'error') console.error('BROWSER ERROR', message.text());
});
page.on('pageerror', error => console.error('PAGE ERROR', error.message));
await page.goto('http://127.0.0.1:3000/example', { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('save-icon')).toBeVisible();
});
Use the final response URL, not just the source string in the markup. For an image, inspect currentSrc, naturalWidth, and complete; a completed image with zero natural width did not decode as a usable image.
const imageState = await page.locator('img[data-testid="save-icon"]').evaluate(img => ({
src: img.getAttribute('src'),
currentSrc: img.currentSrc,
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight
}));
console.log(imageState);
3. Fix URL, origin, and SVG dependency problems
Relative CSS URLs resolve relative to the stylesheet that contains the declaration, while document-relative asset references resolve against the document base URL. Confirm the actual resolved URL and compare it with the application’s deployed path. MDN’s CSS url() reference describes URL resolution and context. If an asset is served from another origin, check the browser’s console and the relevant response headers; cross-origin rules depend on how the resource is used.
A page opened directly with file:// can behave differently from the app’s normal origin and encounter local-origin restrictions. Run the application through its ordinary HTTP development server or the same test server used by CI.
SVG referenced as an image is subject to restrictions on content inside the SVG, including scripts and external images or stylesheets. A file that looks complete when opened directly may omit those dependencies when used through <img> or a CSS background. Make the asset self-contained, or inline the necessary content if the parent document needs to style it. See MDN’s SVG image guide.
- Verify the asset URL after redirects and check that the response contains SVG data rather than an HTML error page.
- Check for a wrong base path after navigation, especially in apps hosted below a path prefix.
- For CSS, open the stylesheet and resolve its relative URL from the stylesheet’s location.
- Remove external dependencies from SVG image assets, or inline the SVG when those dependencies are required.
- Reproduce under the same HTTP origin and route as the screenshot run.
4. Make icon-font loading deterministic
For font-based icons, first establish that the font request succeeds and the intended font is applied. Then wait for the document’s font set to settle before asserting or capturing. The browser’s document.fonts.ready promise resolves when font loading and associated layout operations have completed; it is a synchronization point, not a repair for a failed request. Refer to the MDN CSS Font Loading API.
import { test, expect } from '@playwright/test';
test('capture after icon font is ready', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/example', { waitUntil: 'domcontentloaded' });
await page.evaluate(async () => {
await document.fonts.ready;
});
await expect(page.getByTestId('save-icon')).toBeVisible();
await page.screenshot({ path: 'example.png' });
});
If the font still fails, inspect its request URL, response, CORS behavior, and validity. If the document preloads a font with <link rel="preload">, ensure its CORS mode matches the eventual font fetch. MDN notes that font preloads require a crossorigin attribute; see the preload reference.
<link rel="preload" href="/assets/icons.woff2" as="font" type="font/woff2" crossorigin>
Use a font-loading check to distinguish “not loaded yet” from “failed or not defined.” Adapt the family name to the one declared by the application:
const fontState = await page.evaluate(async () => {
await document.fonts.ready;
return {
status: document.fonts.status,
iconFontAvailable: document.fonts.check('16px "App Icons"')
};
});
console.log(fontState);
document.fonts.check() is useful evidence, but it does not prove that the correct glyph is visible. Also assert the icon element and, where practical, verify the rendered screenshot or a stable visual state.
5. Wait for the page’s actual ready state
Wait for the UI condition that means the page is ready for your screenshot: for example, the results panel is visible, the icon element has appeared, or a loading indicator has gone away. A fixed sleep is brittle because it guesses how long the machine and network need. networkidle is also a poor general readiness signal for tests; Playwright’s Frame API marks it discouraged and recommends web assertions to assess readiness instead.
import { test, expect } from '@playwright/test';
test('capture the ready state', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/example');
await expect(page.getByRole('heading', { name: 'Account settings' })).toBeVisible();
await expect(page.getByTestId('save-icon')).toBeVisible();
await page.screenshot({ path: 'settings.png' });
});
For visual regression checks, Playwright’s toHaveScreenshot() waits for two consecutive screenshots to match before comparison. Pair that stabilization with an assertion that the expected page and icon state exists. See the PageAssertions API.
await expect(page.getByTestId('save-icon')).toBeVisible();
await expect(page).toHaveScreenshot('settings.png');
6. Use a complete diagnostic example
This runnable test collects evidence, waits for fonts, asserts the intended state, and saves a screenshot. It assumes a Playwright Test project with the application available at the stated local URL and a stable data-testid on the icon.
import { test, expect } from '@playwright/test';
test('icon is present in screenshot', async ({ page }) => {
page.on('requestfailed', request => {
console.error(`Request failed: ${request.url()} (${request.failure()?.errorText})`);
});
page.on('response', response => {
if (response.status() >= 400) {
console.error(`HTTP ${response.status()}: ${response.url()}`);
}
});
page.on('console', message => {
if (message.type() === 'error') console.error(`Console: ${message.text()}`);
});
page.on('pageerror', error => console.error(`Page error: ${error.message}`));
await page.goto('http://127.0.0.1:3000/example', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => document.fonts.ready);
await expect(page.getByTestId('save-icon')).toBeVisible();
const state = await page.getByTestId('save-icon').evaluate(element => ({
tag: element.tagName,
box: element.getBoundingClientRect().toJSON(),
fontFamily: getComputedStyle(element).fontFamily,
backgroundImage: getComputedStyle(element).backgroundImage,
outerHTML: element.outerHTML
}));
console.log('Icon state:', state);
await page.screenshot({ path: 'icon-diagnostic.png', fullPage: true });
});
For an SVG <img>, add the image-state inspection from section 2. For an inline SVG, inspect the SVG and its path dimensions and computed paint properties. For a font glyph, check the font request and family state as well as waiting for document.fonts.ready.
7. Keep visual comparisons reproducible
A screenshot can change even when application code does not. Browser version, host operating system, installed fonts, browser settings, hardware, power source, and headless mode can affect rendering. Playwright lists these as potential visual variation sources in its visual comparisons guide. Generate baselines and comparison screenshots in a consistent environment, and make that environment explicit in CI.
- Use the same Playwright browser build for baseline creation and comparison.
- Keep the operating system and required fonts consistent.
- Keep viewport, device scale factor, color scheme, and other screenshot settings consistent.
- Wait for the relevant UI and font readiness before both baseline and current captures.
- When an icon differs, investigate whether the asset or font request changed before updating the baseline.
8. Common errors and fixes
| Symptom | Likely cause | What to do |
|---|---|---|
| Asset request is 404 | Incorrect path, deployment prefix, or URL resolved from an unexpected base | Inspect the final URL; correct the asset path or app base URL. |
| Request returns HTML instead of SVG or font data | Server fallback or error route returned a page with a success-like status | Check response content type and body; fix routing or asset serving. |
| Font request is blocked or fails | Origin/CORS configuration, invalid font response, or inaccessible URL | Inspect browser console and response; serve the font from the intended origin with compatible access settings. |
| Font is preloaded but still fetched again or ignored | Preload CORS mode does not match the eventual font request | Add the appropriate crossorigin attribute and align modes. |
| SVG looks fine in a tab but not as an image | SVG uses scripts, external images, or stylesheets unavailable in image context | Make it self-contained or inline it if the page needs those dependencies. |
| CSS icon URL is missing | URL is relative to the stylesheet location, not the assumed document path | Resolve the URL from the stylesheet’s URL and correct it. |
| Longer timeout changes nothing | Permanent asset, origin, or markup failure rather than a timing issue | Inspect request status, console errors, computed styles, and dimensions. |
| Icon appears locally but fails in CI | Different browser, operating system, installed fonts, configuration, or headless setup | Match the rendering environment and install or bundle required assets. |
Test hangs on networkidle |
Ongoing requests prevent the idle condition | Wait for a semantic UI assertion instead of global network inactivity. |
9. Performance, reliability, and cost
Asset diagnosis has little runtime cost when event listeners and inspection are limited to the failing test. Avoid adding long sleeps to every screenshot; they make suites slower and still leave failures unexplained. Waiting for one meaningful page assertion and, where relevant, font readiness is usually more reliable than waiting for the whole network to go quiet.
For stable visual regression, keep browser and font inputs fixed and retain request/error logs when a capture fails. A screenshot service can simplify capture infrastructure, but it cannot make an invalid or inaccessible application asset valid; confirm the page’s response and expected state in the chosen rendering environment.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. For a one-off capture, call its API with the page URL; the request returns an image or PDF. Read the ScreenshotNeo API documentation for options such as output format, viewport, waiting conditions, custom CSS, headers, cookies, and selectors.
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}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners and removes more than 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 are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes screenshot, page-info, and PDF tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
11. Frequently asked questions
Does document.fonts.ready guarantee the icon font loaded?
No. It indicates that the document’s font loading and layout work have settled. Check the font request and whether the intended family is available if the glyph remains missing.
Should I use waitUntil: 'networkidle' before every screenshot?
No. Playwright discourages it as a general test readiness mechanism. Assert the page state that matters and wait for that condition.
Why does an SVG display directly but not in an <img>?
Image-context SVG has restrictions on scripts and external resources. Make dependencies self-contained or use inline SVG when the document needs to control them.
Why is the screenshot different in CI when the page works locally?
Fonts, operating system, browser version, settings, and headless rendering can differ. Align the baseline and CI rendering environments before treating the difference as an application regression.


