Build a Web Font Checker Chrome Extension: Complete Developer’s Guide
Creating a web font checker extension is one of the most valuable tools you can build for web developers and designers who care about typography performance and consistency. A well-designed webfont checker extension can analyze which fonts are loading on any webpage, detect font loading failures, measure font loading performance, and provide actionable insights for optimizing typography. This comprehensive guide will walk you through building a production-ready Web Font Checker Chrome extension using Manifest V3, modern JavaScript patterns, and best practices for developer tools.
Whether you’re building this extension as a side project, adding to your portfolio, or creating a tool to solve real problems in your development workflow, this guide covers everything from project setup to advanced features like font performance analysis and cross-browser compatibility checks. By the end of this tutorial, you’ll have a fully functional extension that can inspect, analyze, and report on web typography with professional-grade accuracy.
Why Build a Web Font Checker Extension
Web typography plays a crucial role in user experience, brand identity, and accessibility. However, managing fonts on the web is surprisingly complex. Font files must be loaded from servers, parsed by the browser, and rendered correctly before users see the intended typography. When things go wrong, websites can experience Flash of Invisible Text (FOIT), layout shifts when fonts finally load, or complete font rendering failures that leave pages unreadable.
A webfont checker extension solves these problems by giving developers and designers immediate visibility into what’s happening with typography on any webpage. Unlike browser developer tools that bury font information deep in the inspector, a dedicated extension puts font analysis at your fingertips. Users can check if a specific font is loading correctly, see how long fonts take to render, identify fallback font chains, and detect potential accessibility issues with font sizing or contrast.
The demand for such tools is significant. Web developers constantly battle with font loading issues, design teams need to verify that fonts render correctly across different browsers and devices, and accessibility auditors need to check whether text remains readable if custom fonts fail to load. Building a webfont checker extension addresses all these needs while teaching you valuable skills in Chrome extension development, DOM manipulation, and performance analysis.
Extension Architecture Overview
Before diving into code, let’s understand the architecture that makes a web font checker extension work effectively. A well-structured extension follows the three-context model that Chrome extensions use, separating concerns between the service worker, popup interface, and content scripts that run on web pages.
The service worker acts as the background controller, managing state, handling extension lifecycle events, and coordinating communication between different parts of the extension. The popup provides the user interface where users view font analysis results and interact with the extension’s features. The content script runs directly on web pages, scanning the DOM to extract font information, measuring font loading times, and detecting typography patterns.
This separation allows each component to focus on its specific responsibilities. The content script handles the delicate work of inspecting the page without interfering with page functionality. The service worker manages data persistence and complex analysis that might take time. The popup provides a clean, responsive interface for viewing results. Understanding this architecture is essential before writing any code, as it influences every decision from manifest configuration to message passing patterns.
Setting Up the Manifest V3 Configuration
Every Chrome extension begins with the manifest.json file that declares the extension’s capabilities, permissions, and structure. For a web font checker extension, we need careful permission selection to balance functionality with user privacy and Chrome Web Store approval requirements.
{
"manifest_version": 3,
"name": "Web Font Checker",
"version": "1.0.0",
"description": "Analyze font loading performance and detect typography issues on any webpage",
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}]
}
The permission choices here are intentional. We request activeTab and scripting permissions to inject and execute code on the current page, which is necessary for font analysis. The storage permission lets us save user preferences and cache analysis results. The broad <all_urls> host permission is necessary because users will want to check fonts on any website they visit.
However, be aware that the <all_urls> permission may require you to explain your extension’s need for this access when submitting to the Chrome Web Store. Document clearly why your extension needs to access all websites—specifically, that it needs to analyze typography on any webpage users visit. This is a legitimate use case for developer tools, and Chrome generally approves such requests with proper justification.
Content Script: Scanning Font Information
The content script is the heart of font detection. It runs in the context of the webpage and can access the DOM to extract font information that wouldn’t otherwise be visible to extension code. This is where we’ll implement the core logic for detecting fonts, measuring loading performance, and gathering typography data.
// content.js - Font Scanner for Web Font Checker Extension
(function() {
'use strict';
// Detect all fonts used on the page
function detectFonts() {
const fonts = new Map();
const elements = document.querySelectorAll('*');
elements.forEach(element => {
const computedStyle = window.getComputedStyle(element);
const fontFamily = computedStyle.fontFamily;
const fontSize = computedStyle.fontSize;
const fontWeight = computedStyle.fontWeight;
const fontStyle = computedStyle.fontStyle;
// Extract primary font family name
const primaryFont = fontFamily.split(',')[0].trim().replace(/['"]/g, '');
if (primaryFont && primaryFont !== 'inherit') {
if (!fonts.has(primaryFont)) {
fonts.set(primaryFont, {
family: primaryFont,
occurrences: 0,
sizes: new Set(),
weights: new Set(),
styles: new Set(),
elements: []
});
}
const fontData = fonts.get(primaryFont);
fontData.occurrences++;
fontData.sizes.add(fontSize);
fontData.weights.add(fontWeight);
fontData.styles.add(fontStyle);
fontData.elements.push({
tag: element.tagName,
class: element.className,
id: element.id
});
}
});
return Array.from(fonts.values()).map(f => ({
...f,
sizes: Array.from(f.sizes),
weights: Array.from(f.weights),
styles: Array.from(f.styles),
elementCount: f.elements.length
}));
}
// Detect web fonts (Google Fonts, Adobe Fonts, custom web fonts)
function detectWebFontSources() {
const sources = [];
// Check for link tags with font stylesheets
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
const href = link.href || '';
if (href.includes('fonts.googleapis.com') ||
href.includes('fonts.gstatic.com') ||
href.includes('typekit') ||
href.includes('adobe-fonts') ||
href.includes('typography.com')) {
sources.push({
type: 'stylesheet',
url: href,
provider: detectProvider(href)
});
}
});
// Check for @font-face rules in style sheets
const styleSheets = document.styleSheets;
try {
Array.from(styleSheets).forEach(sheet => {
try {
const rules = sheet.cssRules || sheet.rules;
if (rules) {
Array.from(rules).forEach(rule => {
if (rule.type === CSSRule.FONT_FACE_RULE) {
sources.push({
type: '@font-face',
fontFamily: rule.style.fontFamily,
src: rule.style.src,
provider: 'custom'
});
}
});
}
} catch (e) {
// Cross-origin stylesheet, skip
}
});
} catch (e) {
console.log('Could not access all stylesheets');
}
// Check for inline font definitions
document.querySelectorAll('style').forEach(style => {
const text = style.textContent || '';
if (text.includes('@font-face')) {
sources.push({
type: 'inline-style',
provider: 'inline'
});
}
});
return sources;
}
// Detect font provider from URL
function detectProvider(url) {
if (url.includes('fonts.googleapis.com') || url.includes('fonts.gstatic.com')) {
return 'Google Fonts';
} else if (url.includes('typekit') || url.includes('adobe-fonts')) {
return 'Adobe Fonts';
} else if (url.includes('typography.com')) {
return 'Typography.com';
} else if (url.includes('fonts.com')) {
return 'Fonts.com';
}
return 'Custom';
}
// Measure font loading performance
function measureFontLoadingPerformance() {
const performanceData = {
documentLoadTime: performance.timing?.loadEventStart - performance.timing?.navigationStart || 0,
fontsLoaded: 0,
fontLoadingDuration: 0,
fontLoadErrors: [],
criticalFonts: []
};
// Check document.fonts status
if (document.fonts && document.fonts.status) {
performanceData.fontLoadingDuration = performanceData.documentLoadTime;
performanceData.fontsLoaded = document.fonts.size || 0;
}
// Listen for font loading events
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => {
performanceData.fontsLoaded = document.fonts.size || 0;
performanceData.fontLoadingDuration = performance.now();
});
}
return performanceData;
}
// Get comprehensive font analysis
function getFontAnalysis() {
const analysis = {
detectedFonts: detectFonts(),
webFontSources: detectWebFontSources(),
performance: measureFontLoadingPerformance(),
pageTitle: document.title,
pageUrl: window.location.href,
timestamp: new Date().toISOString()
};
return analysis;
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'analyzeFonts') {
const analysis = getFontAnalysis();
sendResponse(analysis);
}
return true;
});
})();
This content script provides comprehensive font detection capabilities. The detectFonts() function walks through every element on the page, using getComputedStyle() to determine which fonts are actually being rendered. It builds a map of all unique fonts, tracking not just the font family but also the sizes, weights, and styles used with each font. This gives users a complete picture of typography usage on the page.
The detectWebFontSources() function goes deeper, examining where fonts are loaded from. It checks <link> tags for external font stylesheets, parses CSS @font-face rules to find custom web fonts, and identifies which font providers are being used. This information is crucial for diagnosing font loading issues—if a font isn’t loading, knowing where it’s supposed to come from helps identify the problem.
Service Worker: Managing Analysis Requests
The service worker acts as the bridge between the content script and the popup interface. It handles the communication flow, manages state, and can perform additional analysis that doesn’t require direct page access. Here’s how to implement the background service worker:
// background.js - Service Worker for Web Font Checker Extension
// Store recent analyses for quick access
let cachedAnalysis = null;
let currentTabId = null;
// Handle messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'analyzeCurrentTab') {
analyzeCurrentTab(message.tabId).then(sendResponse);
return true;
}
if (message.action === 'getCachedAnalysis') {
sendResponse(cachedAnalysis);
return true;
}
if (message.action === 'clearCache') {
cachedAnalysis = null;
sendResponse({ success: true });
return true;
}
});
// Analyze the current tab
async function analyzeCurrentTab(tabId) {
try {
// Inject content script if not already present
const results = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
// This function runs in the context of the page
// We'll collect all the font data here
const getFontAnalysis = () => {
const fonts = new Map();
const elements = document.querySelectorAll('*');
elements.forEach(element => {
const computedStyle = window.getComputedStyle(element);
const fontFamily = computedStyle.fontFamily;
const primaryFont = fontFamily.split(',')[0].trim().replace(/['"]/g, '');
if (primaryFont && primaryFont !== 'inherit') {
if (!fonts.has(primaryFont)) {
fonts.set(primaryFont, {
family: primaryFont,
occurrences: 0,
sizes: new Set(),
weights: new Set()
});
}
const fontData = fonts.get(primaryFont);
fontData.occurrences++;
fontData.sizes.add(computedStyle.fontSize);
fontData.weights.add(computedStyle.fontWeight);
}
});
return Array.from(fonts.values()).map(f => ({
...f,
sizes: Array.from(f.sizes),
weights: Array.from(f.weights)
}));
};
return getFontAnalysis();
}
});
if (results && results[0] && results[0].result) {
cachedAnalysis = {
fonts: results[0].result,
timestamp: new Date().toISOString(),
tabId: tabId
};
currentTabId = tabId;
return cachedAnalysis;
}
return { error: 'Could not analyze fonts' };
} catch (error) {
console.error('Font analysis error:', error);
return { error: error.message };
}
}
// Handle extension installation
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Web Font Checker extension installed');
}
});
// Handle tab updates to refresh analysis if needed
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tabId === currentTabId) {
// Optionally auto-refresh analysis
}
});
The service worker uses chrome.scripting.executeScript() to run code in the context of the target page. This is the recommended approach for Manifest V3 extensions, replacing the older chrome.tabs.executeScript() method. The service worker caches analysis results so users can quickly revisit previous analyses without re-scanning the page.
Popup Interface: Displaying Results
The popup provides the user-facing interface where users interact with the extension. A well-designed popup should present font analysis results clearly, with options for further investigation and export capabilities. Here’s an implementation using vanilla JavaScript:
<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Font Checker</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 380px;
padding: 16px;
background: #ffffff;
color: #333;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.header h1 {
font-size: 18px;
font-weight: 600;
}
.analyze-btn {
width: 100%;
padding: 12px;
background: #4285f4;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.analyze-btn:hover {
background: #3367d6;
}
.analyze-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.results {
margin-top: 16px;
}
.font-card {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.font-name {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
color: #202124;
}
.font-details {
font-size: 12px;
color: #5f6368;
}
.font-stat {
display: inline-block;
margin-right: 12px;
margin-bottom: 4px;
}
.font-stat-label {
color: #80868b;
}
.loading {
text-align: center;
padding: 20px;
color: #5f6368;
}
.error {
background: #fce8e6;
color: #c5221f;
padding: 12px;
border-radius: 6px;
margin-top: 12px;
font-size: 13px;
}
.stats-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stat-box {
background: #f1f3f4;
padding: 12px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #4285f4;
}
.stat-label {
font-size: 11px;
color: #5f6368;
text-transform: uppercase;
}
</style>
</head>
<body>
<div class="header">
<h1>🔍 Web Font Checker</h1>
</div>
<button id="analyzeBtn" class="analyze-btn">Analyze Fonts on This Page</button>
<div id="loading" class="loading" style="display: none;">
Analyzing fonts...
</div>
<div id="error" class="error" style="display: none;"></div>
<div id="results" class="results" style="display: none;">
<div class="stats-summary">
<div class="stat-box">
<div class="stat-value" id="totalFonts">0</div>
<div class="stat-label">Total Fonts</div>
</div>
<div class="stat-box">
<div class="stat-value" id="totalElements">0</div>
<div class="stat-label">Elements</div>
</div>
</div>
<div id="fontList"></div>
</div>
<script src="popup.js"></script>
</body>
</html>
The popup uses a clean, modern design with a clear hierarchy. The analyze button is prominent, and results are organized into a summary section showing total fonts and affected elements, followed by individual font cards showing details about each detected font.
// popup.js - Popup Logic for Web Font Checker
document.addEventListener('DOMContentLoaded', () => {
const analyzeBtn = document.getElementById('analyzeBtn');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const resultsEl = document.getElementById('results');
const fontListEl = document.getElementById('fontList');
const totalFontsEl = document.getElementById('totalFonts');
const totalElementsEl = document.getElementById('totalElements');
analyzeBtn.addEventListener('click', analyzePage);
async function analyzePage() {
// Show loading state
analyzeBtn.disabled = true;
loadingEl.style.display = 'block';
errorEl.style.display = 'none';
resultsEl.style.display = 'none';
try {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || !tab.id) {
throw new Error('No active tab found');
}
// Send message to content script
const response = await chrome.tabs.sendMessage(tab.id, { action: 'analyzeFonts' });
if (response && response.detectedFonts) {
displayResults(response);
} else if (response && response.error) {
throw new Error(response.error);
} else {
throw new Error('Could not analyze this page. Try refreshing the page.');
}
} catch (error) {
errorEl.textContent = error.message;
errorEl.style.display = 'block';
} finally {
analyzeBtn.disabled = false;
loadingEl.style.display = 'none';
}
}
function displayResults(data) {
const fonts = data.detectedFonts || [];
// Calculate totals
const totalElements = fonts.reduce((sum, font) => sum + font.occurrences, 0);
totalFontsEl.textContent = fonts.length;
totalElementsEl.textContent = totalElements;
// Clear previous results
fontListEl.innerHTML = '';
// Sort fonts by usage (most used first)
const sortedFonts = [...fonts].sort((a, b) => b.occurrences - a.occurrences);
// Display each font
sortedFonts.forEach(font => {
const fontCard = createFontCard(font);
fontListEl.appendChild(fontCard);
});
resultsEl.style.display = 'block';
}
function createFontCard(font) {
const card = document.createElement('div');
card.className = 'font-card';
const nameEl = document.createElement('div');
nameEl.className = 'font-name';
nameEl.textContent = font.family;
const detailsEl = document.createElement('div');
detailsEl.className = 'font-details';
// Add font occurrences
const occEl = document.createElement('span');
occEl.className = 'font-stat';
occEl.innerHTML = `<span class="font-stat-label">Used:</span> ${font.occurrences} times`;
detailsEl.appendChild(occEl);
// Add font sizes if available
if (font.sizes && font.sizes.length > 0) {
const sizesEl = document.createElement('span');
sizesEl.className = 'font-stat';
sizesEl.innerHTML = `<span class="font-stat-label">Sizes:</span> ${font.sizes.slice(0, 3).join(', ')}${font.sizes.length > 3 ? '...' : ''}`;
detailsEl.appendChild(sizesEl);
}
// Add font weights
if (font.weights && font.weights.length > 0) {
const weightsEl = document.createElement('span');
weightsEl.className = 'font-stat';
weightsEl.innerHTML = `<span class="font-stat-label">Weights:</span> ${font.weights.join(', ')}`;
detailsEl.appendChild(weightsEl);
}
card.appendChild(nameEl);
card.appendChild(detailsEl);
return card;
}
});
Advanced Features: Font Performance Analysis
A truly useful webfont checker extension goes beyond simple detection and provides actionable insights about font loading performance. Here are some advanced features you can implement to make your extension stand out:
Font Loading Performance Metrics
Measuring font loading performance helps developers identify bottlenecks. You can detect Flash of Invisible Text (FOIT) by monitoring when fonts become available versus when content renders. Track the time between page load and font availability using the Font Loading API:
// Advanced font performance analysis
async function analyzeFontPerformance() {
const performance = {
fontLoadTimes: [],
foitDetected: false,
fontSwapOccurred: false,
recommendations: []
};
if (!document.fonts || !document.fonts.ready) {
performance.recommendations.push('Font Loading API not fully supported');
return performance;
}
// Wait for fonts to load
await document.fonts.ready;
// Check each font's loading status
for (const fontFace of document.fonts) {
const loadStatus = {
family: fontFace.family,
status: fontFace.status,
loadStart: fontFace.loadStart,
loadEnd: fontFace.loadEnd
};
if (fontFace.loadEnd) {
loadStatus.loadDuration = fontFace.loadEnd - fontFace.loadStart;
}
performance.fontLoadTimes.push(loadStatus);
}
// Generate recommendations based on analysis
const slowFonts = performance.fontLoadTimes.filter(f => f.loadDuration > 500);
if (slowFonts.length > 0) {
performance.recommendations.push(
`${slowFonts.length} font(s) took longer than 500ms to load. Consider using font-display: swap or preloading.`
);
}
return performance;
}
Font Fallback Detection
Analyzing fallback font chains helps ensure text remains readable even if custom fonts fail to load. This is crucial for accessibility and provides recommendations for better fallback configurations:
function analyzeFallbackFonts() {
const fallbacks = [];
const elements = document.querySelectorAll('*');
const analyzed = new Set();
elements.forEach(element => {
const computedStyle = window.getComputedStyle(element);
const fontFamily = computedStyle.fontFamily;
if (!analyzed.has(fontFamily)) {
analyzed.add(fontFamily);
const fonts = fontFamily.split(',').map(f => f.trim().replace(/['"]/g, ''));
fallbacks.push({
requested: fonts[0],
fallback: fonts.slice(1).join(', ') || 'system default',
isGeneric: fonts.some(f => ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'].includes(f.toLowerCase()))
});
}
});
return fallbacks;
}
Google Fonts Integration
Many websites use Google Fonts, and your extension can provide specific insights about these commonly used fonts:
function identifyGoogleFonts(sources) {
const googleFonts = sources.filter(s =>
s.provider === 'Google Fonts' ||
(s.url && s.url.includes('fonts.googleapis.com'))
);
return googleFonts.map(f => {
// Extract font name from Google Fonts URL
let fontName = '';
if (f.url) {
const match = f.url.match(/family=([^:&]+)/);
if (match) {
fontName = match[1].replace(/\+/g, ' ');
}
}
return {
...f,
fontName
};
});
}
Testing Your Extension
Before releasing your extension, thorough testing ensures it works correctly across different types of websites. Test your extension on pages with various font configurations:
Test on pages using Google Fonts to verify detection works correctly. Test on pages with custom self-hosted fonts using @font-face rules. Test on pages with multiple font weights and styles to ensure all variations are detected. Test on pages with font loading failures to verify error handling. Test on complex websites with iframes, Shadow DOM, and dynamic content injection.
You can test your extension locally by navigating to chrome://extensions/, enabling Developer mode, clicking “Load unpacked”, and selecting your extension’s directory. Use the popup to analyze different websites and verify the results match what you see in browser developer tools.
Publishing to Chrome Web Store
Once your extension is tested and ready, you can publish it to the Chrome Web Store. Prepare your store listing with clear screenshots showing the extension in action, a compelling description that explains the value of your tool, and appropriate categories and tags to help users discover your extension.
You’ll need to pay a one-time developer registration fee of $5 to publish to the Chrome Web Store. Create a ZIP file of your extension directory (excluding unnecessary files), upload it through the Developer Dashboard, and wait for Google’s review process, which typically takes a few hours to a few days.
Conclusion
Building a web font checker extension is an excellent project that teaches you valuable skills in Chrome extension development while creating a genuinely useful tool for web developers and designers. The extension architecture we covered—separating concerns between content scripts, service workers, and popup interfaces—applies to virtually any Chrome extension you might build in the future.
The key concepts you learned include how to use the DOM and computed styles to detect fonts, how to measure font loading performance using the Font Loading API, how to structure a Manifest V3 extension with proper permissions, and how to create a clean, responsive popup interface. These skills transfer directly to other developer tools and extension projects.
As you continue developing your extension, consider adding features like font comparison tools, export capabilities for design handoffs, integration with design system documentation, or even AI-powered typography recommendations. The web font checker extension you build today can grow into a comprehensive typography toolkit that serves developers and designers for years to come.