· guides, reference
How websites detect your installed fonts
Every font you have ever installed is, in principle, visible to the websites you visit. Not the files themselves, but the fact that they are there: your system fonts, the Office and Adobe fonts that rode in with other software, and every free font you have ever downloaded and double-clicked, including the ones from sites like this one’s sister catalog. Pages have wanted that list for two very different reasons: tools like Wordmark want to show your own words set in your own fonts, and fingerprinters want a stable way to recognize your browser without cookies.
There are two ways to get it, sixteen years apart in spirit. The first is running live below; the second now has a tool of its own. Run the probe and see what it learns about you. Nothing either one finds leaves your browser.
Method one: the ruler (2009)
No permission, no API. The page measures a hidden test string in every candidate font it can think of, and infers which ones you have.
Not run yet. It can only test names on its list, of them.
Method two: ask the browser (2022)
The Local Font Access API returns your actual font list, every family and style, but only after the browser asks your permission. This demo has graduated into a standalone tool: the Local Font Browser lists your installed fonts the way a font manager does, with a live specimen of every face.
Open the Local Font Browsermmmmmmmmmmlli AaBbGg 1234567890
mmmmmmmmmmlli AaBbGg 1234567890 (the same text in your browser's plain serif, for comparison)
The probe runs entirely on this page, in your browser, and the Local Font Browser keeps the same rule on its own page. Nothing either one finds is uploaded, stored, or sent to font.io or anyone else. That is a choice, not a limitation: the code could send the list anywhere, which is the whole subject of the post below.
Method one: measure, and infer
The browser gives a page no way to ask “is Arial installed?” So in 2009, Lalit Patel published a detector that never asks. It measures.
The trick works because of CSS fallback. Set a test string in "Some Font", monospace and the browser renders it in Some Font if that font exists, and in monospace if it does not. Render the same string in monospace alone and compare the sizes. Different widths mean the first rendering was not monospace, so the font must be present. One comparison can be fooled by a font whose metrics happen to match the fallback, so the detector tests against three generic baselines, monospace, sans-serif, and serif, and trusts a mismatch against any of them.
Two details from the original are worth stealing. The test string is "mmmmmmmmmmlli": m is the widest letter in most fonts, so a long run of them amplifies tiny metric differences, and the l and i at the end separate fonts that agree on the m’s but differ elsewhere. And the test size is a huge 72px, for the same reason: measure big, and small differences become whole pixels.
Here is the whole technique, cleaned up but faithful:
const BASES = ["monospace", "sans-serif", "serif"];
const span = document.createElement("span");
span.textContent = "mmmmmmmmmmlli";
span.style.cssText =
"position:absolute;left:-9999px;visibility:hidden;" +
"white-space:nowrap;font-size:72px;";
document.body.appendChild(span);
const defaults = {};
for (const base of BASES) {
span.style.fontFamily = base;
defaults[base] = [span.offsetWidth, span.offsetHeight];
}
function fontInstalled(name) {
for (const base of BASES) {
span.style.fontFamily = `"${name}", ${base}`;
if (span.offsetWidth !== defaults[base][0] ||
span.offsetHeight !== defaults[base][1]) {
return true; // something other than the fallback rendered
}
}
return false;
}
Patel’s code became the canonical answer to the obvious question, preserved in the most-linked Stack Overflow answer on the subject, and variants of it powered a generation of “preview in your fonts” features.
Know its limits, because they are structural:
- It can only confirm names it guesses. There is no enumeration. A font the list does not name does not exist as far as the probe is concerned, which is why the demo above tests stock Windows and Mac fonts plus a shelf of popular downloads, and still cannot see the long tail of what a font collector actually installs.
- It has false negatives. A candidate whose test string measures exactly like all three fallbacks reads as absent. Rare per font, real across a list.
- Names are slippery. You test family names as CSS knows them, and full names, localized names, and style-specific names do not always match what you guessed.
Why fingerprinters adopted it
A font list is close to a fingerprint all by itself. Stock systems share stock fonts, but the accumulated extras, the fonts that came with a printer driver, a design suite, a game, a language pack, are close to unique per machine. In the EFF’s Panopticlick study, more than four in five browsers could be distinguished from every other, and the font list was one of the strongest signals in the mix. The probe needs no permission, shows no prompt, and leaves no trace, so trackers folded it into their scripts wholesale: run a few hundred candidate names, hash the yes answers, and you hold an identifier that survives cookie deletion. You can watch the demo above compute that hash. Cover Your Tracks, the EFF’s current test, will show you the same signal from the other side.
Same code, two intentions. The difference between a feature and a fingerprint is not the technique; it is whether the list stays on the machine and whether the person was asked.
The counterattack: make everyone look alike
The Tor Browser’s answer was to lie by standardization: a font.system.whitelist meant pages could only ever see a fixed set of fonts, identical for every user, so the measurement always returned the same answer. Firefox adopted the approach in 2017, and its recent releases go further: Firefox 145, in late 2025, began blocking attempts to read local fonts in Private Browsing and Strict tracking protection, as part of a broader program that roughly halved the share of users who could be fingerprinted. Safari likewise never offered an enumeration API. The ruler still works in ordinary browser profiles, as the demo shows, but its accuracy now depends on how privacy-hardened the visitor’s browser is, and it will keep decaying.
Method two: ask, with permission
Chrome took the other door: instead of letting pages keep guessing, give them a real API and put a permission prompt in front of it. The Local Font Access API shipped in Chrome 103 and is documented in Chrome’s guide to local fonts and on MDN. One call returns the complete list, not the candidates the page thought to guess:
const fonts = await window.queryLocalFonts();
for (const font of fonts) {
console.log(font.family); // "Prisma"
console.log(font.fullName); // "Prisma Regular"
console.log(font.postscriptName); // "Prisma-Regular"
console.log(font.style); // "Regular"
}
Every result is a FontData object with the family, full name, PostScript name, and style, and you can pass a postscriptNames option to ask about specific faces only. A blob() method goes further and hands over the font’s raw data, which is how a tool like Wordmark renders accurate previews of fonts it has never heard of: the browser supplies the actual file, in the page, for that session.
The conditions are the point. The call only works on a secure page, only in response to a user gesture like a click, and the browser shows a permission prompt the site cannot style, pre-answer, or bypass. Deny it and the page gets an exception and learns nothing. Today the API ships in Chrome and Edge on desktop; Firefox and Safari have not shipped it. The working version of this method now lives in its own tool, the Local Font Browser, which turns the raw list into a browsable set of families and specimens.
The API that does not do this
One nearby tool deserves a warning, because it looks like a detector and is not. document.fonts.check("16px Prisma") belongs to the Font Loading API, which answers questions about fonts the page itself has declared and loaded. It was never an inventory tool, and browsers will cheerfully return true for family names that exist nowhere on the system, since nothing about an unknown name requires loading. Generations of developers have wired it up as an installed-font test and measured nonsense. If you need to know whether a specific local font exists and the Local Font Access API is unavailable, the unfashionable ruler is still the honest tool.
Which one belongs in your code
| The ruler (measurement) | Local Font Access API | |
|---|---|---|
| Returns | Yes or no, for names you supply | Every installed face, with metadata |
| Permission | None, and no prompt | Browser prompt, per site |
| Coverage | Stock browsers today; blocked or standardized in hardened ones | Chrome and Edge on desktop |
| Failure mode | Silent false negatives | A visible “no” |
Rules of thumb, whichever you use. Prefer the API where it exists, because a permission prompt is a better foundation than a side channel. If you probe, probe only the names your feature genuinely needs, not the longest list you can find. And treat the resulting list as the sensitive data it is: keep it on the device, use it in the session, and never ship it to a server, not even as a hash. The hash is not an anonymized list; the hash is the fingerprint, which is rather the point of the last sixteen years.
Every font you install changes what pages can infer about you; the install guides on this site are, read from the other direction, a list of ways that footprint grows. The web platform spent a decade and a half moving this capability from a measurement hack nobody consented to, to an API that has to ask. Build on the asking version.
Sources: Lalit Patel’s original detector and write-up; the Local Font Access specification; MDN, Window.queryLocalFonts; EFF, Cover Your Tracks.