Afina

Download app

AppleWindows
EN

How Browsers Detect Installed Fonts and Build a Font Fingerprint

A browser checks which installed fonts it can detect on a device

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:

  1. Take wide and narrow characters, for example mmmmmmmmmmlliWW@@.
  2. Measure them in monospace, serif, and sans-serif.
  3. Put the target name in front of each fallback.
  4. Compare the width and the bounding box, meaning the frame around the text.
  5. 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.

Diagram of the fallback text width measurement method used to detect fonts

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:

  1. Copy the entire code block above, paste it into the Console tab in DevTools, and press Enter.
  2. Confirm the console shows "Code loaded successfully! Now just click...".
  3. Click an empty spot on the web page itself, or next to some text, not inside the DevTools panel.
  4. Click "Allow" in the browser's permission prompt.
  5. 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 names
  • FontData.blob() returns SFNT data after permission is granted, meaning the actual contents of the .ttf/.otf container, 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.

SurfaceChromium desktopFirefoxSafari and WebKit
fallback metrics and Canvasavailable without extra protectionprotection may hide local fonts or alter Canvasavailable for the allowed set; user-installed fonts are hidden
document.fonts.check()readiness, not installationsame logicsame logic
queryLocalFonts()desktop versions, after permissionnot availablenot available
privacy modeIncognito doesn't change every signalPrivate Browsing and ETP Strict strengthen protectionuser-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.

Diagram of how installed fonts shape a browser fingerprint and visitor identification

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.

BrowserWhere to lookWhat it shows
Chrome, Afina, and other Chromium-based browsersElements → Computed → Rendered Fontsthe fonts actually used and their glyph count
FirefoxInspector → Fonts → Fonts usedfonts used by the element and the page
SafariElements → Fonts sidebarthe 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:

  1. Open DevTools with F12 or Ctrl+Shift+I on Windows, Cmd+Opt+I on Mac. Or right-click the text and choose Inspect.
  2. Open Elements, usually the first tab.
  3. 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.
  4. Open Computed on the right, next to Styles. If the panel isn't there, expand DevTools.
  5. 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.

Computed panel in Chrome DevTools with the Rendered Fonts section for the selected element

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.

SymptomLikely causeWhat to do
the OS sees the font, the site doesn'tthe browser was open before installationclose every window and process, then reopen
the font is missing in another accountCurrent User, not All Userscheck that account or install for all users if you have the rights and license
regular works, bold doesn'tbold is missing or being fakedcheck the font manager and Rendered Fonts
the file name doesn't workthe filename doesn't match the family/PostScript namecheck the internal names
the typeface is missing in appsunsupported formatfind the .ttf/.otf; for web use, convert a properly licensed font to WOFF2, don't simply rename the file extension
errors or the font disappearsthe file is corruptedcheck it with the font manager and reinstall from a trusted source
Chrome sees it, Safari doesn'tWebKit hides user-installed fontsconnect a licensed web font via @font-face or a fallback
Firefox Strict shows fewerstrengthened protectiondisable it only for diagnostics, then turn it back on
individual characters look differentthe needed glyphs are missingcheck that text in Rendered Fonts

Checking on Windows:

  1. Press Win+I and open Personalization → Fonts.
  2. Find the name and open its card to see the styles.

Alternative path:

  1. Open File Explorer with Win+E.
  2. Type C:\Windows\Fonts in the address bar and press Enter.
  3. Use the search box in the top-right corner.

On macOS, use the built-in Font Book app:

  1. Press Command+Space, type Font Book, and open the app.
  2. Turn on View → Show Sidebar and select a collection.
  3. Find the typeface, its styles, characters, and internal data.
  4. Activate a disabled font via Edit → Activate.
  5. Check the file with File → Validate Selection: green means it passed, yellow is a warning, red is an error.
  6. 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.

Font fingerprinting test results page with a list of detected fonts and a uniqueness score

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:

  1. Type browserleaks.com/fonts into the address bar of the profile you want to test.
  2. Wait 5–15 seconds for Font Metrics and Unicode Glyphs to finish loading, without switching tabs.
  3. Record the font report/count and the fingerprint. Keep the test name and these values in the screenshot.
  4. Close the profile, launch a different Afina profile, and repeat the test.
  5. Compare the data. Different sites aren't directly comparable.

To keep the comparison fair:

  1. Use the same checker every time; record the browser, OS, and protection level.
  2. Don't change the page or screen scale.
  3. Wait for web fonts loaded via @font-face; they aren't local fonts.
  4. 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-fonts requests 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:

  1. "Font name" field. A single-line field with a placeholder like Arial or Roboto and a short hint to enter the family name, not the .ttf or .otf file name.
  2. "Check" button. After clicking, it runs the measurement of one test string across several fallback families.
  3. "Candidate font" block. Shows the test string and its width in pixels after placing the entered name ahead of the fallback.
  4. "Fallback text" block. Shows the same string, size, and width without the candidate font.
  5. 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:

  1. Open Afina and select "Accounts" on the left.
  2. Click ⋮ → Edit for the profile. For a new one, choose "Add Account", then "New Account".
  3. 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.
  4. Click "Cancel" to leave it unchanged, or "Save" after making changes. "Generate New Fingerprint" changes the profile.
  5. Select the profile in "Accounts", click "Launch", and wait for the browser to open.
  6. Type browserleaks.com/fonts into the address bar, wait 5–15 seconds, and record the font report/count and the fingerprint.
  7. Rerun the test; repeat it in a different Afina profile and compare.
  8. 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.

Download

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.

Related terms

Continue reading onAnti-detect browser — profile isolation | Afina Browser
Vladyslav Shestakov

Hello! I'm Vladyslav Shestakov - a data analysis and automation expert at Afina. Focused on web automation, product support, and development. I have experience in cryptocurrency, machine learning, and creating custom bots and automation tools. Combining technical expertise with continuous self-improvement and integration of modern technologies to make working with Web3 efficient and understandable.