Fix chromedp Screenshots with Missing Web Fonts in Go
Wait for the page’s fonts after its content renders, then check that the intended face loaded before capturing with chromedp.
A visible element does not mean its web fonts have loaded. In chromedp, wait for the page content you need, await the browser’s font set readiness, and then check that the specific font face is available before capturing. Font readiness means loading and layout have settled; it does not prove that a particular remote font succeeded.
This guide shows a runnable Go pattern, how to check a desired font, and what to investigate if the screenshot still uses a fallback. chromedp controls Chrome or Chromium through the DevTools Protocol. Its DOM visibility waits and screenshot actions address different conditions, so sequence them explicitly. See the chromedp package documentation and project guidance.
1. Wait for content, then wait for fonts
Run the font readiness check after navigation and after any application-specific render step that inserts or changes the text. The following example waits for a report element, waits for document.fonts.ready, checks whether Roboto is available for representative text, and captures the full page.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
if err := capture("https://example.com", "#report", "report.png"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func capture(url, selector, output string) error {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
// chromedp starts Chrome or Chromium using its default allocator settings.
ctx, cancelBrowser := chromedp.NewContext(ctx)
defer cancelBrowser()
var fontStatus struct {
Ready bool `json:"ready"`
HasRoboto bool `json:"hasRoboto"`
}
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitVisible(selector, chromedp.ByQuery),
// If your app renders the target text later, wait for that app-specific
// condition here, before asking the browser about fonts.
chromedp.Evaluate(`(async () => {
await document.fonts.ready;
return {
ready: document.fonts.status === "loaded",
hasRoboto: document.fonts.check('16px "Roboto"', 'Report heading')
};
})()`, &fontStatus),
chromedp.FullScreenshot(&image, 90),
)
if err != nil {
return fmt.Errorf("capture page: %w", err)
}
if !fontStatus.Ready {
return fmt.Errorf("document font set did not reach loaded status")
}
if !fontStatus.HasRoboto {
return fmt.Errorf("Roboto was not available for the checked text; inspect font requests and CSS")
}
if err := os.WriteFile(output, image, 0644); err != nil {
return fmt.Errorf("write screenshot: %w", err)
}
return nil
}
Save this as main.go, replace the example URL, selector, and font check with values from your page, then run go mod init screenshot-font-check, go get github.com/chromedp/chromedp, and go run .. The selector uses chromedp.ByQuery, so it can be a CSS selector such as #report or .report. Ensure Chrome or Chromium is installed and available to chromedp in your environment.
The JavaScript is passed as a promise-returning expression to chromedp.Evaluate; chromedp waits for the result and unmarshals it into the Go struct. The code is a practical pattern, not a diagnosis of every missing-font case. Check the chosen text and CSS font shorthand against the actual page: a font face can be loaded but not selected for the element, or the check can be unrepresentative of the text and face in question.
2. What each wait establishes
| Signal | What it establishes | What it does not establish |
|---|---|---|
WaitVisible |
The queried DOM node is visible according to chromedp’s query conditions. | That the application finished rendering all text, or that web fonts loaded. |
| Application-specific condition | Your page reached a state you define, such as a report row appearing or a loading indicator disappearing. | That a requested font URL succeeded. |
document.fonts.ready |
Font loading and layout operations for the document have settled. | That every desired font loaded successfully or that CSS selected it. |
document.fonts.check(...) |
Whether the browser reports the specified font shorthand and text as available. | Why a face is unavailable, or that every element uses the intended face. |
| Network and console inspection | Evidence about requests and browser errors that can explain a failed font load. | A universal explanation without inspecting the page and its environment. |
Chromedp documents node readiness and visibility as DOM query conditions, and separately documents element and full-page screenshot actions. The reported issue #1474 describes a remote Roboto @font-face and a visible-element screenshot, but provides no confirmed diagnosis or resolution. Do not assume it proves a chromedp defect or that a specific wait fixed it: chromedp issue #1474.
3. Handle delayed client rendering
If your application inserts text after navigation, waiting for an initially visible container is too early. Wait for the actual content or application state first, then wait for fonts. For example, when a loading marker is removed only after the report is rendered:
chromedp.Navigate(url),
chromedp.WaitVisible("#report", chromedp.ByQuery),
chromedp.WaitNotPresent(".report-loading", chromedp.ByQuery),
chromedp.Evaluate(`document.fonts.ready.then(() => true)`, nil),
chromedp.FullScreenshot(&image, 90),
Use a condition that matches your application. If it replaces text inside an existing visible element, checking visibility alone cannot detect that change. A page-specific JavaScript condition can wait for the expected text or state, followed by the font readiness check. Keep the overall context deadline bounded so a page that never reaches the target state returns an error rather than waiting forever.
Network idle may help on pages where it is meaningful, but it is not proof that the target text rendered or that the intended font loaded. Pages with long-lived requests can also make network-idle conditions unsuitable. Prefer a signal tied to the content you need, then check the font set.
4. Diagnose a fallback font
- Confirm the target text exists before the font check. If text is inserted after the check, wait for the application’s render-complete condition and check readiness again.
- Check the intended face. Use
document.fonts.check('16px "Roboto"', 'the actual text')with the family, style, weight, and text relevant to the element. A false result means the check did not find the requested face available for that query; it does not identify the cause. - Inspect the browser’s font request. Use DevTools Protocol logging or a browser debugging session to inspect the request URL, status, and console messages. Check whether the stylesheet containing
@font-faceloaded and whether the face’s URL is reachable from the Chrome process. - Check stylesheet and face selection. Verify the family name, weight and style descriptors, URL, and the CSS rule applied to the captured element. A successfully loaded font file does not guarantee that the element selects that face.
- Check cross-origin and runtime network behavior. If the font is hosted on another origin, inspect the response and browser console for cross-origin errors. Also check whether the machine or container running Chrome can resolve and reach the host. These are diagnostic possibilities, not causes established by issue #1474.
- Recheck after any page changes. If scripts change classes, styles, or text after the initial readiness check, wait for that state and inspect the face again before capture.
For a focused browser-side diagnostic, evaluate a small result object and log it from Go:
var diagnostic struct {
Status string `json:"status"`
Roboto bool `json:"roboto"`
}
err := chromedp.Run(ctx,
chromedp.Evaluate(`({
status: document.fonts.status,
roboto: document.fonts.check('16px "Roboto"', 'Report heading')
})`, &diagnostic),
)
if err != nil {
return err
}
fmt.Printf("font status=%s roboto available=%t\n", diagnostic.Status, diagnostic.Roboto)
For browser-level evidence, enable the DevTools Network and Console panels in a debugging session, or add DevTools Protocol event logging to your chromedp workflow. Look specifically for the font file request and the stylesheet that declares it. Avoid treating an HTTP success alone as proof of correct rendering: the browser must also select the intended face for the target text.
5. Common errors and fixes
| Symptom | Likely interpretation | Next step |
|---|---|---|
Screenshot uses a fallback although WaitVisible passed |
The wait established node visibility, not font readiness. | Wait for the app’s content, await document.fonts.ready, then check the desired face. |
document.fonts.ready resolves but the face check is false |
Readiness settled, but the queried face is not available for that shorthand and text. | Inspect the font request, CSS declaration, face descriptors, and browser console. |
| The face check is true but appearance is still wrong | The checked face may not match the element’s actual style, weight, text, or selected CSS rule. | Inspect computed styles and verify the specific element and font descriptors. |
| Context deadline exceeded | Navigation, app rendering, or a wait condition did not finish within the deadline. | Identify which action timed out, check page/network access, and adjust the bounded deadline to the workflow. |
| Element query times out | The selector may be wrong, the element may be in another frame, or the app may not have rendered it. | Verify the selector and frame, then wait for the right application state. |
| Font request fails in Chrome | The browser could not use the requested resource; possible causes include URL, network, response, or cross-origin configuration. | Inspect the exact request and console error from the Chrome environment. |
| Works locally but not in a container | The Chrome process may have different network access, certificates, or installed browser setup. | Inspect requests and logs in the same container and runtime used for capture. |
6. Capture and reliability notes
- Use explicit state, not a guessed sleep. Fixed delays can be too short on slow runs and waste time on fast ones. chromedp project guidance recommends waiting for required page state.
- Bound each run. Put navigation, rendering waits, font readiness, and capture under a context deadline. Return errors with the stage name so callers can distinguish timeout from file-write failure.
- Keep the capture after validation. Check font state before screenshot capture, and fail or record a diagnostic when a required face is unavailable rather than silently accepting a fallback.
- Use the right screenshot action. Choose an element screenshot when only a particular node is needed; use a full-page action when the whole document is required. Confirm the action’s output and quality settings in the package API documentation.
- Do not assume a universal cost or performance figure. Runtime depends on the page, browser startup, network, font host, and capture size. Reuse a browser process where your service design permits, while keeping page contexts and deadlines isolated.
7. Or skip the browser setup
For a one-call capture without managing Chrome and chromedp, ScreenshotNeo is a website screenshot API and MCP server. It returns PNG, JPEG, WebP, or PDF from one GET request. Its clean-shot flow accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.
cURL:
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', new Uint8Array(await res.arrayBuffer()));
See the ScreenshotNeo API documentation for authentication and request options. The API also supports full-page and element captures, custom CSS and JavaScript, waits, browser settings, caching, asynchronous jobs, and bulk captures. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
ScreenshotNeo includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. Create a free account and get 1,000 screenshots a month with no card.
8. FAQ
Does a resolved document.fonts.ready promise mean my web font loaded?
No. It indicates font loading and layout operations have settled. Check the specific face and inspect its request to establish whether it loaded and was selected.
Should I always wait for network idle?
No. It can be an additional signal on suitable pages, but persistent requests and delayed client rendering make it unreliable as a universal definition of page readiness.
Does chromedp issue #1474 prove a chromedp font bug?
No. The issue report documents a remote Roboto setup and screenshot scenario, but the cited report does not give a confirmed cause or resolution.
Can I use the same font check for every page?
No. Match the CSS shorthand and text to the font family and content your target element should render.


