How Browsers Detect Installed Fonts and Build a Font Fingerprint

A font fingerprint is the set of local font traits a site can use to distinguish one browser or device environment from another. Sites use it to check layout and to supplement the browser fingerprint, but the result depends on the browser's permissions and protections.
Like handwriting, the same text takes on a different shape and width. Let's see how to check this.
Can websites detect installed fonts
Yes, but mostly indirectly. Without explicit permission, a site can't read your fonts folder and doesn't receive a list of files from disk. It compares how the browser draws the same string in different typefaces, and from the difference in width, text box, or pixels it concludes that a specific font is likely available. This is a heuristic, so it can produce false readings: an installed font with metrics close to the fallback may go undetected, while aliases or other font-resolution behavior can complicate the result.
A direct listing exists only in desktop Chromium through queryLocalFonts(), and it requires explicit user permission and transient user activation, such as a click. Firefox and Safari don't have this API, and WebKit additionally hides user-installed fonts from sites. So the answer depends on the browser: in one case a site sees only indirect traces, in another it gets the full list exposed to it once permission is granted.
What a site actually sees when checking local fonts
Without permission, a page doesn't get access to C:\Windows\Fonts or the Font Book catalog. It sees indirect traces of font data based on how text looks.
The source can be the OS, a local install, or @font-face. Generic families like serif, sans-serif, monospace, cursive, fantasy, and system-ui don't point to a specific file. cursive means a handwriting-style face, fantasy a decorative one; the OS and browser pick the actual typeface.
This is called font matching. When no candidate is available, the browser falls back to a fallback font. A glyph is the drawn shape of a character.
A browser can:
- pick a font by its CSS name
- read metrics: the width, height, and position of letters
- load a web font
- return the local fonts exposed to that browser and site via
queryLocalFonts()after permission is granted
Text being ready to display still doesn't prove Arial exists on disk.
How sites detect a font without asking for permission
A script iterates over names and compares the size or pixels of rendered text. Canvas fingerprinting works similarly, but geometry and pixels produce different signals.
How the fallback comparison works
The browser draws a line of text with the fallback font and with the candidate font, then compares them:
- Take wide and narrow characters, for example
mmmmmmmmmmlliWW@@. - Measure them in
monospace,serif, andsans-serif. - Put the target name in front of each fallback.
- Compare the width and the bounding box, meaning the frame around the text.
- Mark the font as likely available if at least one pair changed.
But identical metrics across different typefaces can cause false negatives. Multiple fallbacks, sizes, and a height check reduce errors but never reveal the full catalog.

What this code does: it invisibly draws the same string in different fonts and compares their dimensions. You don't need to run the code to understand the idea.
const ctx = document.createElement("canvas").getContext("2d");
const sample = "mmmmmmmmmmlliWW@@";
const fallbacks = ["monospace", "serif", "sans-serif"];
function metrics(fontStack) {
ctx.font = `72px ${fontStack}`;
const m = ctx.measureText(sample);
return [m.width, m.actualBoundingBoxAscent, m.actualBoundingBoxDescent].join("|");
}
function fontLooksAvailable(family) {
return fallbacks.some((fallback) => {
const baseline = metrics(fallback);
const candidate = metrics(`"${family}", ${fallback}`);
return candidate !== baseline;
});
}
console.log(fontLooksAvailable("Arial"));
The Canvas API gives the browser a canvas to draw on, and measureText() returns the size of the rendered text. A site can quietly check hundreds of names this way.
How font metrics differ from a pixel-based Canvas hash
Metrics show size. A Canvas fingerprint reads the actual drawn pixels via getImageData() or toDataURL() and builds a hash from them.
Pixels depend on anti-aliasing, the graphics system, the file itself, and hinting, meaning how letters are adjusted to fit the screen's pixel grid. Identical width doesn't guarantee an identical image. A browser can alter Canvas readback, meaning how the finished image is read, without changing what the user sees. Font visibility restrictions, however, may cause the page to use a fallback font instead of the installed one.
Can you check a local font via CSS local()
src: local() looks for a typeface by its full name or PostScript name, for example HelveticaNeueLTPro-Roman. Without a match, the browser moves on to the next source.
When a web developer wants to use a font installed on the user's computer, they add a rule like this directly to the site's .css stylesheet. In this example, the browser first looks for Helvetica Neue on the device, and if it isn't found, loads the fallback file from the site.
@font-face {
font-family: "ProbeFont";
src:
local("Helvetica Neue"),
local("HelveticaNeue"),
url("fallback.woff2") format("woff2");
}
Because of the fingerprinting risk, WebKit hides user-installed fonts, and Firefox does the same in its stricter protection modes.
Why document.fonts.check() doesn't prove a font is installed
document.fonts.check() checks whether text can render without waiting, not whether a file exists on the computer. It helps avoid font swap, a visible replacement of the fallback by a web font once it loads.
What this code does: it asks the browser whether Arial text is ready to display without waiting. A true value doesn't prove Arial is installed locally.
const available = document.fonts.check('16px "Arial"');
console.log(available);
A made-up font name also returns true if the browser immediately falls back. This means "no need to wait," not "the file exists."
For a web font declared through @font-face, run the check after the fonts have finished loading.
What this code does: it waits until the page finishes working with fonts, then checks whether a specific sample can render without a sudden font swap.
If the console shows a paused in the debugger error while you're testing this yourself, page execution is paused. Look for the yellow banner at the top of the browser window and click the blue Play (Resume script execution) button to unpause the page, then run the code again.
const fontSpec = '16px "MyWebFont"';
const sample = "BESbswy";
await document.fonts.ready;
const readyWithoutSwap = document.fonts.check(fontSpec, sample);
console.log({ readyWithoutSwap });
Add the characters you actually need: Cyrillic, Latin, Chinese characters, or emoji. Otherwise other glyphs get checked through the fallback instead.
When it makes sense to use queryLocalFonts()
window.queryLocalFonts() returns the family, full name, PostScript name, and style to web editors. It requires permission and transient user activation, a brief activation triggered by a click or another action. It can't be requested in the background.
What this code does: once pasted into Console, it waits for the first click anywhere on the web page itself, requests access to local fonts, and prints the allowed names. No separate button on the site is needed.
// Waits for a click anywhere on the page
document.body.addEventListener("click", async () => {
if (!("queryLocalFonts" in window)) {
console.log("Local Font Access API is not supported");
return;
}
try {
const fonts = await window.queryLocalFonts();
for (const font of fonts) {
console.log(font.family, font.fullName, font.postscriptName, font.style);
}
console.log(`Successfully found fonts: ${fonts.length}`);
} catch (error) {
console.error(error.name, error.message);
}
}, { once: true }); // Runs only on the first click
console.log("Code loaded successfully! Now just left-click anywhere on the web page itself (not in the console) to see the list of fonts.");
How to test this code yourself:
- Copy the entire code block above, paste it into the Console tab in DevTools, and press Enter.
- Confirm the console shows "Code loaded successfully! Now just click...".
- Click an empty spot on the web page itself, or next to some text, not inside the DevTools panel.
- Click "Allow" in the browser's permission prompt.
- Switch back to the Console tab: the full list of local fonts available to this browser will appear instantly, along with a summary message showing the count.
The request requires HTTPS; a site or browser can block it. The result doesn't necessarily cover the entire catalog. On desktop Chromium, check support via 'queryLocalFonts' in window. Firefox, Safari, and mobile Chromium don't have this API. Capabilities:
queryLocalFonts({ postscriptNames: [...] })requests specific internal namesFontData.blob()returns SFNT data after permission is granted, meaning the actual contents of the.ttf/.otfcontainer, not just the name
Without permission, only indirect traces are available; with the Local Font Access API, font metadata and, through FontData.blob(), the underlying SFNT font data.
How font detection differs across Chromium, Firefox, and Safari
Chromium offers a permission-based API, Firefox strengthens protection in special modes, and WebKit hides user-installed fonts.
| Surface | Chromium desktop | Firefox | Safari and WebKit |
|---|---|---|---|
| fallback metrics and Canvas | available without extra protection | protection may hide local fonts or alter Canvas | available for the allowed set; user-installed fonts are hidden |
document.fonts.check() | readiness, not installation | same logic | same logic |
queryLocalFonts() | desktop versions, after permission | not available | not available |
| privacy mode | Incognito doesn't change every signal | Private Browsing and ETP Strict strengthen protection | user-installed fonts are hidden by default, not only in private mode; Private Browsing additionally adds noise to 2D Canvas and WebGL readback |
Firefox's fingerprinting protections can restrict visibility of non-standard fonts, introduce noise when canvas images are read back, and block known fingerprinting scripts, depending on the enabled protection mode.

WebKit restricts fonts added by the user but allows system and web fonts. That's why Chrome and Safari differ on Mac.
How to find out which font the browser actually rendered
DevTools shows the font used for a fragment along with its fallback.
| Browser | Where to look | What it shows |
|---|---|---|
| Chrome, Afina, and other Chromium-based browsers | Elements → Computed → Rendered Fonts | the fonts actually used and their glyph count |
| Firefox | Inspector → Fonts → Fonts used | fonts used by the element and the page |
| Safari | Elements → Fonts sidebar | the primary font, its style, and variations |
font-family: Inter, Arial, sans-serif sets a queue of candidates. Rendered Fonts shows the actual choice.
To find Rendered Fonts in Chrome or an open Afina profile:
- Open DevTools with F12 or Ctrl+Shift+I on Windows, Cmd+Opt+I on Mac. Or right-click the text and choose Inspect.
- Open Elements, usually the first tab.
- Click the HTML element of the text. Alternative: click the arrow-in-a-box icon in the top-left corner, hover over the text, and click.
- Open Computed on the right, next to Styles. If the panel isn't there, expand DevTools.
- Scroll Computed to the bottom and expand Rendered Fonts. This shows the actual font used and its glyph count.
A single line can combine Inter for Latin characters, a system emoji font, and a fallback for a missing Cyrillic character. At step 3, pick the problematic fragment.

Why a file name doesn't match the CSS font-family
A single file has several names:
File: HelveticaNeueLTPro-Roman.otf
Family: Helvetica Neue LT Pro
Full name: Helvetica Neue LT Pro Roman
PostScript name: HelveticaNeueLTPro-Roman
font-family uses the family name, src: local() uses the full name or PostScript name, and queryLocalFonts({ postscriptNames }) uses the exact PostScript name. The .otf or .ttf file name may not match any of them.
Why the browser doesn't see a font you just installed
The main causes: the browser hasn't restarted, the name is wrong, or fingerprinting protection is active.
| Symptom | Likely cause | What to do |
|---|---|---|
| the OS sees the font, the site doesn't | the browser was open before installation | close every window and process, then reopen |
| the font is missing in another account | Current User, not All Users | check that account or install for all users if you have the rights and license |
| regular works, bold doesn't | bold is missing or being faked | check the font manager and Rendered Fonts |
| the file name doesn't work | the filename doesn't match the family/PostScript name | check the internal names |
| the typeface is missing in apps | unsupported format | find the .ttf/.otf; for web use, convert a properly licensed font to WOFF2, don't simply rename the file extension |
| errors or the font disappears | the file is corrupted | check it with the font manager and reinstall from a trusted source |
| Chrome sees it, Safari doesn't | WebKit hides user-installed fonts | connect a licensed web font via @font-face or a fallback |
| Firefox Strict shows fewer | strengthened protection | disable it only for diagnostics, then turn it back on |
| individual characters look different | the needed glyphs are missing | check that text in Rendered Fonts |
Checking on Windows:
- Press Win+I and open Personalization → Fonts.
- Find the name and open its card to see the styles.
Alternative path:
- Open File Explorer with Win+E.
- Type
C:\Windows\Fontsin the address bar and press Enter. - Use the search box in the top-right corner.
On macOS, use the built-in Font Book app:
- Press Command+Space, type
Font Book, and open the app. - Turn on View → Show Sidebar and select a collection.
- Find the typeface, its styles, characters, and internal data.
- Activate a disabled font via Edit → Activate.
- Check the file with File → Validate Selection: green means it passed, yellow is a warning, red is an error.
- Resolve duplicates with File → Resolve Duplicates. Whether it's Current User or All Users is shown in Font Book → Settings → Installation.
On Linux, fc-list shows the list, and fc-match "Arial" shows the substituted file. Then check the actual choice in Rendered Fonts.
How local fonts turn into a font fingerprint
A script combines the font map with Canvas, WebGL, the screen, and other traits. Fingerprint checkers use different lists and algorithms.
Rarity of the combination matters most: a rare font can contribute considerably more identifying entropy than common system fonts.

Adobe apps, office suites, and CAD software add distinctive font families. They hint at the software installed, but don't prove someone's profession or identity.
Fonts fingerprint helps identify a browser when a stable set lines up with other signals.
How to compare results correctly and reduce fingerprinting
Testing on BrowserLeaks:
- Type
browserleaks.com/fontsinto the address bar of the profile you want to test. - Wait 5–15 seconds for Font Metrics and Unicode Glyphs to finish loading, without switching tabs.
- Record the font report/count and the fingerprint. Keep the test name and these values in the screenshot.
- Close the profile, launch a different Afina profile, and repeat the test.
- Compare the data. Different sites aren't directly comparable.
To keep the comparison fair:
- Use the same checker every time; record the browser, OS, and protection level.
- Don't change the page or screen scale.
- Wait for web fonts loaded via
@font-face; they aren't local fonts. - Restart the profile and the test. Stability matters most.
Why the number of detected fonts changes
The count can change without you installing anything manually:
- OS and app updates add, update, or remove typefaces
- language packs add Cyrillic, Arabic, CJK, and other scripts
- Adobe Fonts and other cloud services activate fonts before you notice, or turn syncing off
- corporate policies change fonts on work devices
- browser permissions:
local-fonts, private mode, and protection settings change the visible set - test dictionaries: one site checks 100 names, another checks 500
Repeat the test on the same site and profile, then check the OS, apps, and permissions.
How to limit local font checks
You can reduce the amount of data exposed:
- reject
local-fontsrequests via "Block" unless you actually need a tool that requires the list - turn on the built-in protection in Firefox, WebKit, or another browser
- keep cookies, cache, and other data separated between profiles; they don't remove system fonts, so the configuration should stay stable
- block JavaScript only as a last resort: menus, forms, video, sign-in, and online tools will stop working
Font substitution or disabling JavaScript can break the design, characters, and readability. Change protection settings gradually.
Incognito isolates cookies and history but doesn't necessarily change font metrics.
An unusual font set that is inconsistent with the reported OS or other fingerprint signals can make the overall fingerprint more distinctive.
Mini test: does the browser see a specific font
You can place a test like this directly in an article so readers don't need to open Console. The user types a font name, and the page compares the size of identical text rendered with the candidate font versus the fallback font.
Structure of the interactive block for the developer building it:
- "Font name" field. A single-line field with a placeholder like
ArialorRobotoand a short hint to enter the family name, not the.ttfor.otffile name. - "Check" button. After clicking, it runs the measurement of one test string across several fallback families.
- "Candidate font" block. Shows the test string and its width in pixels after placing the entered name ahead of the fallback.
- "Fallback text" block. Shows the same string, size, and width without the candidate font.
- Cautious result. If the sizes differ, show "Likely available." If there's no difference, show "Could not confirm." Don't use definitive statuses like "Found" or "Not found."
Below the result, add a short explanation: the test works as a heuristic, meaning it makes a guess based on the size difference. It doesn't scan the disk and doesn't prove that a specific font file is installed on the system.
How to control the font signal in isolated profiles
A site cross-references fonts with Canvas, WebGL, cookies, and the network. A profile needs to produce a consistent, stable result.
For anti-detect profiles, consistency matters more than simply reducing the number of visible fonts. A Windows profile should expose font behavior that is plausible for Windows, and the same profile should produce stable results across launches. A rare but internally consistent font set can be less suspicious than a randomized set that conflicts with the reported OS, Canvas, or other signals.
Potential fingerprint consistency mismatches include:
- a Windows User-Agent paired with a font set found only on macOS
- a macOS fingerprint paired with Windows-specific typefaces
- a font set that changes between two launches of the same profile
- Canvas rendering that disagrees with the measured font metrics
Afina isolates the fingerprint, proxy, cookies, and cache within a Chromium profile for fingerprint management. There's no list of local fonts in the settings: the signal gets checked after launch.
How to check the fingerprint settings and the result in a specific Afina profile:
- Open Afina and select "Accounts" on the left.
- Click ⋮ → Edit for the profile. For a new one, choose "Add Account", then "New Account".
- Check the OS, User-Agent, CPU, memory, Canvas, WebGL, Audio, and Rects under "General" → "Browser Fingerprint". Font exposure should be evaluated as part of the complete fingerprint rather than as an isolated setting.
- Click "Cancel" to leave it unchanged, or "Save" after making changes. "Generate New Fingerprint" changes the profile.
- Select the profile in "Accounts", click "Launch", and wait for the browser to open.
- Type
browserleaks.com/fontsinto the address bar, wait 5–15 seconds, and record the font report/count and the fingerprint. - Rerun the test; repeat it in a different Afina profile and compare.
- Open the service's Canvas and WebGL tests in each profile. Judge stability and consistency across all the signals, not a single "green" score.
Conclusion
Most font detection methods remain heuristic: a site compares metrics and pixels and draws a probabilistic conclusion rather than reading a list of files from disk. The Local Font Access API provides more precise data, but it is strictly gated by user permission and transient activation, and it exists only in desktop Chromium. Limiting unnecessary permissions such as local-fonts, enabling built-in browser protections, and keeping profiles isolated all help reduce the amount of data a site can collect, though none of these steps works on its own. Reliable protection against tracking depends on the overall consistency of the entire browser fingerprint rather than the number of visible fonts alone: a single font hash proves neither uniqueness nor anonymity.
FAQ — Frequently Asked Questions
Can a site see every font installed on a computer?
Without permission, a site can't see the full font catalog. After permission is granted, queryLocalFonts() returns the list available to the browser.
How does a site detect whether a specific font is installed?
A site compares text size against a fallback or reads Canvas pixels. This indirect check can be wrong.
Why does document.fonts.check() return true for a font that doesn't exist?
document.fonts.check() can return true because the browser immediately substitutes a fallback and doesn't wait for anything to load.
Which browsers support queryLocalFonts()?
queryLocalFonts() works, after permission, in desktop Chromium, but not in Firefox, Safari, or mobile Chromium.
Can a site download my local font file?
Without permission, a site can't read a local file. After permission is granted, FontData.blob() can provide the font's data.
Does incognito mode protect against font fingerprinting?
Incognito mode doesn't fully protect against font fingerprinting. It isolates the session but can still preserve metrics and other signals.
How can I find out which font the browser actually used?
The actual font appears in the Rendered Fonts section. In DevTools, select the text and open Elements → Computed → Rendered Fonts.
How can I reduce font fingerprinting?
To reduce font fingerprinting, reject unnecessary permissions and enable browser protection. Cross-check fonts against Canvas and WebGL.
Can a device or operating system be identified from its font set?
A font set can hint at the OS or installed apps. This signal needs to be cross-checked with Canvas, WebGL, language, and other data.
How does a local font differ from a web font?
A local font is installed in the OS. A web font is loaded by the site via @font-face.
