How I Built a Side-by-Side Font Comparison Tool (And Accidentally Learned Way Too Much About Browser APIs)
Source: Dev.to
The Idea
I built FontPreview because I was tired of guessing how fonts would look with real text. The thing designers kept asking was:
“Can I compare this Google Font with the font already on my computer?”
Turns out, that’s harder than it sounds.
Browsers don’t really want you poking around someone’s system fonts. For good reason — imagine every website you visit getting a list of everything installed on your computer. That’s a privacy nightmare.
But there’s a newer API that lets you do this, if the user says it’s okay.
The Local Font Access API
There’s this thing called the Local Font Access API. It’s relatively new, and not every browser supports it yet (looking at you, Safari). In Chrome and Edge you can do this:
const fonts = await window.queryLocalFonts();
That one line of code returns an array of every font installed on the user’s system.
Important: The browser will ask the user for permission first. A popup appears saying “This site wants to see your fonts.” If the user says No, you get nothing. This is good — we don’t want random sites scraping font lists without permission.
The Permission Dance
Here’s how I handle the permission request:
function checkSystemFontsPermission() {
if (!window.queryLocalFonts) {
showToast('Local Font Access API not supported in this browser');
useFallbackFonts();
return;
}
// Show the permission modal
document.getElementById('permissionModal').style.display = 'flex';
}
- If the browser doesn’t support the API, I fall back to a list of popular system fonts. Not perfect, but better than nothing.
- If it does support it, I show a modal explaining why I’m asking and what I’ll do with the data (spoiler: nothing. I just show the fonts in a dropdown).
What Happens When They Say Yes
When the user clicks Allow, this runs:
async function requestFontPermission() {
try {
const fonts = await window.queryLocalFonts();
// Clean up font names (remove "Regular", "Bold", etc.)
const fontMap = new Map();
fonts.forEach(f => {
let name = f.family;
const suffixes = [' Regular', ' Bold', ' Italic', ' Light', ' Medium'];
suffixes.forEach(suffix => {
if (name.endsWith(suffix)) {
name = name.substring(0, name.length - suffix.length);
}
});
if (!fontMap.has(name)) {
fontMap.set(name, {
family: name,
fullName: f.family,
style: f.style,
weight: f.weight
});
}
});
// Sort and store
allSystemFonts = Array.from(fontMap.values()).sort((a, b) =>
a.family.localeCompare(b.family)
);
showToast(`Loaded ${allSystemFonts.length} system fonts`);
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('Permission denied. Using fallback fonts.');
} else {
showToast('Error loading fonts. Using fallback.');
}
useFallbackFonts();
}
}
The Map is important because many fonts come back with multiple entries for different styles — “Arial Regular”, “Arial Bold”, “Arial Italic”. I only want “Arial” once, so the Map deduplicates them.
The Big Problem I Didn’t Expect
Once I had the font list, I needed to actually use those fonts in the preview.
In CSS you can simply write:
font-family: 'Arial', sans-serif;
If the user has Arial installed, it works. Great.
But I also wanted to let users compare system fonts with Google Fonts side‑by‑side. If someone picks Arial on the left and Roboto on the right, I need to load Roboto from Google Fonts. That means dynamically injecting a <link> tag:
function loadGoogleFontForPanel(fontName, panel) {
const fontFamily = fontName.replace(/ /g, '+');
const linkId = `panel-font-${panel}`;
const oldLink = document.getElementById(linkId);
if (oldLink) oldLink.remove();
const link = document.createElement('link');
link.id = linkId;
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily}&display=swap`;
document.head.appendChild(link);
}
Each panel can load its own font independently: the left panel uses a system font (no stylesheet needed), the right panel loads a Google Font (which does need a stylesheet).
The Part That Still Bugs Me
When you load a Google Font dynamically, there’s a tiny delay before it becomes available. During that moment the text shows up in the default font, then swaps to the chosen font — a flash of unstyled text (FOIT).
I tried a bunch of fixes: preloading, font-display: swap, even hiding the text until the font loads. Everything felt janky. In the end I just left it; the flash is brief and most users don’t notice, but I do, every time.
What I’d Do Differently
If I built this again from scratch:
- Cache the permission status. Right now the modal shows every time you click “System”. Store the user’s choice in
localStorageso we don’t nag them repeatedly. - Better error handling. Sometimes the API just fails silently. I need to catch those cases and fall back gracefully.
- Fallback for Safari. Safari doesn’t support this API at all. Instead of a static list of popular fonts, I could provide a more robust fallback (e.g., a curated set of web‑safe fonts or a downloadable font‑preview bundle).
The Code (If You Want It)
The whole thing is on FontPreview if you want to see it in action.
View source — it’s all client‑side, no backend, no tracking, just HTML, CSS, and JavaScript.
I’m not a great developer. I learned most of this by breaking things and Googling errors. But it works, and people use it, and that’s enough for me.
If you’re building something with the Local Font Access API, hit me up. I’ve probably already hit the same bugs you’re hitting.
Tags: javascript, webdev, tutorial, showdev, api