How to Detect Website Fonts with an API
Detect the fonts a page declares, loads, and actually renders with browser automation, CSS Font Loading API, DevTools Protocol, and network evidence.
How to Detect Website Fonts with an API
The reliable way to detect website fonts is to render the page in a real browser, wait for document.fonts.ready, inspect representative elements with getComputedStyle, enumerate document.fonts, and correlate CSS declarations with network font responses. For Chromium audits, Chrome DevTools Protocol (CDP) can add node-level evidence through CSS.getPlatformFontsForNode.
Keep three results separate in your report:
- Declared: families and faces present in CSS.
- Loaded: font faces that successfully fetched and loaded.
- Observed rendered: fonts the browser used for a particular node in a particular environment.
A font-family value is an ordered fallback list. The first name is not proof that its glyphs rendered. Optional loading, missing files, platform differences, and responsive or dynamic UI can change the result.
1. Build a browser-based detection pipeline
- Record the URL, route, viewport, user agent, locale, browser version, operating system, and timestamp.
- Navigate with a real Chromium instance.
- Trigger the states that matter: menus, modals, lazy content, authentication views, and other interactions.
- Wait for
document.fonts.ready, then inspect font faces and representative text nodes. - Capture computed styles, accessible
@font-facerules, and network responses for font files. - Use CDP platform-font inspection when you need evidence of the face used to render a specific node.
- Normalize families while retaining the original values, source URLs, weights, styles, stretches, unicode ranges, and load failures.
The CSS Font Loading API exposes the document’s FontFaceSet and its loading state. MDN notes that declared and used sets can differ, for example when font-display: optional prevents a face from loading in time (CSS Font Loading API; Document.fonts).
2. Complete Node.js implementation with Playwright
Install Playwright and its browser:
npm install playwright
npx playwright install chromium
Save this as detect-fonts.mjs. It records computed stacks, font-face state, same-origin CSS rules when available, resource URLs, and optional CDP platform-font evidence.
import { chromium } from 'playwright';
const target = process.argv[2] || 'https://example.com';
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
locale: 'en-US',
userAgent: 'font-audit/1.0'
});
const page = await context.newPage();
const fontResponses = [];
page.on('response', response => {
const type = response.request().resourceType();
const url = response.url();
const contentType = response.headers()['content-type'] || '';
if (type === 'font' || /font|woff2?|truetype|opentype/i.test(contentType) || /\.(woff2?|ttf|otf)(\?|$)/i.test(url)) {
fontResponses.push({ url, status: response.status(), contentType });
}
});
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(250);
const report = await page.evaluate(() => {
const selectors = 'h1,h2,h3,p,body,nav,button,a,label,input,textarea,select,[role="heading"]';
const elements = [...document.querySelectorAll(selectors)].slice(0, 200);
const styles = elements.map((el, index) => {
const s = getComputedStyle(el);
return {
index,
tag: el.tagName.toLowerCase(),
text: (el.textContent || '').trim().slice(0, 120),
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
fontStyle: s.fontStyle,
fontStretch: s.fontStretch,
lineHeight: s.lineHeight
};
});
const faces = [...document.fonts].map(face => ({
family: face.family,
style: face.style,
weight: face.weight,
stretch: face.stretch,
status: face.status,
unicodeRange: face.unicodeRange,
display: face.display
}));
const rules = [];
for (const sheet of [...document.styleSheets]) {
try {
for (const rule of [...sheet.cssRules]) {
if (rule.type === CSSRule.FONT_FACE_RULE) rules.push({
cssText: rule.cssText,
family: rule.style.getPropertyValue('font-family'),
src: rule.style.getPropertyValue('src'),
weight: rule.style.getPropertyValue('font-weight'),
style: rule.style.getPropertyValue('font-style'),
stretch: rule.style.getPropertyValue('font-stretch'),
unicodeRange: rule.style.getPropertyValue('unicode-range')
});
}
} catch (error) {
rules.push({ inaccessible: true, reason: String(error), href: sheet.href });
}
}
return { url: location.href, styles, faces, rules };
});
// CDP evidence for the first useful text node.
const cdp = await context.newCDPSession(page);
await cdp.send('DOM.enable');
await cdp.send('CSS.enable');
const node = await cdp.send('DOM.querySelector', { nodeId: 1, selector: 'h1,p,body' }).catch(() => null);
let platformFonts = [];
if (node?.nodeId) {
platformFonts = (await cdp.send('CSS.getPlatformFontsForNode', { nodeId: node.nodeId })).cssFamilyName || [];
}
console.log(JSON.stringify({ capturedAt: new Date().toISOString(), target, report, fontResponses, platformFonts }, null, 2));
await browser.close();
Run it with:
node detect-fonts.mjs https://your-site.example/pricing > font-report.json
For CDP, a production implementation should resolve the document node ID with DOM.getDocument and query descendants from that root. The CSS domain method is documented by the Chrome DevTools Protocol CSS domain.
3. Inspect computed styles correctly
Computed styles answer, “Which CSS stack did this element receive?” They do not always answer, “Which face supplied the glyphs?” Capture the full stack and the properties that select a face:
const style = getComputedStyle(element);
console.log({
family: style.fontFamily,
weight: style.fontWeight,
style: style.fontStyle,
stretch: style.fontStretch,
variationSettings: style.fontVariationSettings,
featureSettings: style.fontFeatureSettings,
size: style.fontSize
});
Inspect more than body. Headings, navigation, buttons, form controls, captions, data tables, and dynamically revealed content commonly use different families or weights. Preserve the element selector or a stable DOM path so another audit can reproduce the observation.
4. Enumerate document.fonts and loading failures
document.fonts contains faces known to the document. Each FontFace exposes family, style, weight, stretch, status, and related descriptors.
await document.fonts.ready;
const faces = [...document.fonts].map(face => ({
family: face.family,
style: face.style,
weight: face.weight,
stretch: face.stretch,
status: face.status,
loaded: face.status === 'loaded'
}));
console.table(faces);
To force a face needed by a test state, call document.fonts.load('700 16px "Family Name"') and check whether the returned array is empty. Do not use this to claim that visible text used the face; it only verifies that the browser can load a matching face.
5. Read @font-face declarations and network evidence
Same-origin stylesheets can be read through CSSOM. Cross-origin stylesheets may throw a security exception, so catch it and retain the sheet URL. Network logs remain useful even when CSSOM is inaccessible.
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.type === CSSRule.FONT_FACE_RULE) {
console.log(rule.style.getPropertyValue('font-family'));
console.log(rule.style.getPropertyValue('src'));
}
}
} catch (error) {
console.warn('Stylesheet inaccessible:', sheet.href, error.message);
}
}
Correlate .woff2, .woff, .ttf, and equivalent responses with the declarations that reference them. Record HTTP status, content type, redirects, cache status where available, and failures. A successful download still does not prove that a visible node used that face.
6. Add rendered-font evidence with CDP
Chromium’s CSS.getPlatformFontsForNode reports platform fonts used for a node. This is stronger than a CSS fallback stack, but it is environment-specific: operating system, browser version, installed fonts, viewport, and user agent all matter. Store those values beside every result.
Use CDP on representative nodes, especially text containing characters that exercise different scripts or fallback behavior. Compare the platform result with the computed family, loaded faces, and network files instead of treating any single signal as authoritative.
7. Normalize the audit output
A useful record keeps original evidence and a normalized summary:
| Field | Purpose |
|---|---|
familyOriginal |
Exact CSS or FontFace family string. |
familyNormalized |
Case and quote normalized for grouping. |
sourceUrls |
Font file and stylesheet URLs. |
weight/style/stretch |
Face descriptors used for matching. |
unicodeRange |
Character coverage constraints. |
declared/loaded/rendered |
Separate confidence states. |
routes/states/elements |
Where the family was observed. |
environment |
Browser, OS, viewport, locale, and user agent. |
Normalize quoted names and whitespace for grouping, but never discard the original string. A site can self-host a Google family under a renamed face, and a local face can share a family name with a system font.
8. Correlate with Google Fonts metadata
Use the Google Fonts Developer API only after page evidence identifies a candidate family. It is a catalog lookup, not a detector. Google documents stylesheet requests and family, style, weight, subset, and text parameters in its getting-started guide and explains that the Fonts API returns a user-agent-specific stylesheet containing @font-face rules (technical considerations).
Never infer usage merely because a family exists in the Google catalog. The page may self-host it, rename it, or use a different provider.
9. Coverage for responsive and dynamic sites
- Audit desktop and mobile viewports; media queries can switch families.
- Visit important routes instead of assuming one page represents the site.
- Open menus, dialogs, tabs, and infinite-scroll regions before collecting evidence.
- Test locale and writing-system changes; fallback can differ by script.
- Inspect shadow DOM components when your automation can access them.
- Repeat after interactions that inject CSS or content.
- Use a stable wait condition in addition to a short delay; network idle alone can be misleading on analytics-heavy pages.
10. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
document.fonts is missing faces |
The face is in an inaccessible frame, shadow tree, or not yet inserted. | Inspect frames and components, trigger the UI state, then await document.fonts.ready. |
| Computed family says A, pixels look like B | A is unavailable, optional, or missing glyphs. | Check load status, platform-font evidence, and characters that trigger fallback. |
Reading cssRules throws |
Cross-origin stylesheet restrictions. | Catch the exception; use network logs, computed styles, and CDP evidence. |
| Font request returns 404 or CORS error | Bad URL, server configuration, or missing CORS headers. | Record the response, verify the URL, and inspect the browser console and response headers. |
| Results change between runs | Different cache state, timing, viewport, browser, or OS. | Pin the environment, capture timestamps, clear or document cache state, and use deterministic waits. |
| Only one weight appears | Other weights are declared but never requested. | Inspect actual elements and call document.fonts.load only for diagnostic coverage. |
| Headless result differs from a user’s browser | Platform fonts and rendering engines differ. | Store environment metadata and treat platform-font output as environment-specific. |
11. Performance, reliability, privacy, and cost
Browser startup dominates small audits. Reuse a browser process, create isolated contexts per job, limit DOM sampling, and collect only the network metadata you need. Waiting for every request to finish can hang on long-lived analytics connections; combine a bounded timeout with document.fonts.ready and an application-specific readiness signal.
For reliability, retry navigation failures with bounded backoff, distinguish DNS/TLS errors from page-level failures, and save partial evidence when a route times out. Run more than one viewport or route when coverage matters. Font files and URLs can contain sensitive information, so define retention and access rules before sending reports to a service.
Self-hosted Chromium costs compute and maintenance. A hosted capture API can remove browser setup, but compare services on rendered-font confidence, dynamic and cross-origin coverage, reproducibility, privacy, and whether they preserve source URLs and load failures.
Or skip the browser setup
ScreenshotNeo provides a browser screenshot API and MCP server. It can capture a page after consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing state.
Use the one-call capture below, then inspect the returned page in your own font-audit workflow. The full option reference is in the ScreenshotNeo documentation.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Every feature is included on every plan: 1,000 shots per month are free with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
12. Short FAQ
Can an API detect a font from HTML alone?
HTML and CSS can reveal declarations, but only a rendered browser can show loading state, fallback behavior, and environment-specific glyph use.
Does a Google Fonts match prove the page uses that font?
No. Match page evidence first, then use Google metadata to enrich the identified family.
Why do two operating systems report different fonts?
Platform fonts, fallback coverage, installed files, and browser versions differ. Store the environment with every observation.
Should I report every declared font?
Report declared, loaded, and observed-rendered sets separately so consumers can distinguish intent from what appeared on screen.
How many elements should an audit inspect?
Sample all typography roles and dynamically revealed states, then cap or prioritize nodes for predictable runtime. The right number depends on page complexity.


