The fastest way to see a website's fonts is DevTools: select an element and check the rendered fonts panel at the bottom of the Computed tab. It names what actually painted, which - and this is the whole subject of this post - is frequently not what the CSS asked for. font-family is a wish list. The browser walks it until something works, and with lazy loading in the mix, what renders can differ from what is declared, per element, per moment.
For anything beyond one element, you want a script. Here are the three signals worth reading, and what each one can and cannot tell you.
Signal 1: computed styles (what CSS resolved to)
// Paste in the console: ranked font stacks in use
Object.entries([...document.querySelectorAll('body *')].reduce((acc, el) => {
const f = getComputedStyle(el).fontFamily;
acc[f] = (acc[f] || 0) + 1;
return acc;
}, {})).sort((a, b) => b[1] - a[1]).slice(0, 5)
This tells you which stacks the cascade assigned where - the heading family vs the body family falls straight out of the counts. What it does not tell you: whether the first font in the stack ever loaded.
Signal 2: document.fonts (what actually loaded)
The FontFace API is the receipt:
[...document.fonts].map(f => `${f.family} ${f.weight} - ${f.status}`)
// e.g. ["Sohne 400 - loaded", "Sohne 700 - loaded", "SohneMono 400 - unloaded"]
Every registered webfont with its weight and load status. An unloaded entry means the CSS declared it but nothing on the page triggered the download - a font the site owns but this page does not use.
Signal 3: the network (where fonts come from)
Filter DevTools' Network tab by "Font" and you get provenance: fonts.gstatic.com means Google Fonts, use.typekit.net means Adobe, files off the site's own domain mean self-hosted (and usually a commercial license).
All three from Python
pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
font_urls = []
page.on("response", lambda r: font_urls.append(r.url)
if r.request.resource_type == "font" else None)
page.goto("https://stripe.com", wait_until="networkidle")
page.evaluate("document.fonts.ready")
loaded = page.evaluate(
"[...document.fonts].map(f => `${f.family} ${f.weight} ${f.status}`)")
used = page.evaluate("""
[...new Set([...document.querySelectorAll('h1,h2,h3,p,a,button')]
.map(el => getComputedStyle(el).fontFamily))]
""")
browser.close()
print("LOADED:", loaded)
print("USED:", used)
print("SERVED FROM:", {u.split('/')[2] for u in font_urls})
The document.fonts.ready await matters. Skip it and a font-display: swap site reports its fallback stack, because you measured during the swap window. This is the single most common reason font detection scripts return Arial for sites that very much do not use Arial.
What each signal can tell you
| Question | Computed styles | document.fonts | Network |
|---|---|---|---|
| Which family is used for headings vs body | Yes | No | No |
| Did the webfont actually load | No | Yes | Yes |
| Weights the site ships | Partially | Yes | Yes |
| Google Fonts vs self-hosted vs Adobe | No | No | Yes |
| Works without executing the page | No | No | No |
Or use one API call
Our fonts endpoint reads the declared font sources from the page's HTML and CSS - @font-face rules, Google Fonts links, preloads - and returns families, weights, and where they are served from:
curl "https://miromiro.app/api/v1/fonts?url=stripe.com" \
-H "Authorization: Bearer $MIROMIRO_API_KEY"
For the full typographic token set (sizes, weights, line heights, letter spacing alongside the families), use /v1/extract instead. There is also a free font extractor if you want to check one site right now with no key. Free key: 100 credits a month, no card.
Summary
font-family declares intent; document.fonts reports reality; the network names the source. Cross-reference at least the first two, always after document.fonts.ready, and be suspicious of any script that confidently reports a system font on a design-forward site - it probably measured mid-swap.
Frequently asked questions
How do I find a website's font without an extension?
DevTools → Computed tab → rendered fonts panel. It names what painted, not what was requested.
Why does font-family show a font that isn't rendering?
Because it is a fallback list. With font-display: swap, the fallback can render and stay whenever the webfont is slow or never loads.
How do I detect fonts from a script?
Playwright, await document.fonts.ready, then read document.fonts and computed fontFamily values together (code above).
Can I use a font I found on a website?
Identifying is fine, using needs a license. Google Fonts yes; a site's self-hosted commercial webfonts are licensed to that site, not to you.
Skip the maintenance - one API call
Design tokens, brand data, fonts, SVGs, images, and section code from any URL. 100 free credits a month, no card.
Soraia · Founder, MiroMiro
Building MiroMiro: a browser extension and API that extract design tokens, assets, and production-ready code from any live website. The gotchas in these posts come from the extraction engine's own commit history.
Follow on X