Chrome Extension Font Settings Api — Best Practices
28 min readFont Settings API in Chrome Extensions
Overview
The Chrome Font Settings API (chrome.fontSettings) enables extensions to read and modify user font preferences at the browser level. This guide covers eight practical patterns from basic font retrieval to accessibility-focused font injection.
Required Permission
{
"name": "Font Settings Extension",
"version": "1.0.0",
"permissions": ["fontSettings"],
"manifest_version": 3
}
Pattern 1: Font Settings API Basics
The API supports six generic font families: standard, sansserif, serif, fixed, cursive, and fantasy.
Getting the Current Font
// background.ts or options.ts
interface FontSettings {
fontId: string;
genericFamily: string;
script?: string;
}
async function getCurrentFont(
genericFamily: string,
script?: string
): Promise<FontSettings> {
const details: { genericFamily: string; script?: string } = { genericFamily };
if (script) details.script = script;
return await chrome.fontSettings.getFont(details);
}
// Usage
const sansSerifFont = await getCurrentFont("sansserif");
console.log(`Current sans-serif: ${sansSerifFont.fontId}`);
Setting a Font
async function setFont(
genericFamily: string,
fontId: string,
script?: string
): Promise<void> {
const details: { genericFamily: string; fontId: string; script?: string } = {
genericFamily,
fontId,
};
if (script) details.script = script;
await chrome.fontSettings.setFont(details);
}
// Usage
await setFont("standard", "Open Sans");
await setFont("fixed", "Fira Code");
Pattern 2: Reading Current Font Configuration
Getting All Installed Fonts
interface FontDescriptor {
fontId: string;
displayName: string;
localizedName?: string;
}
async function getInstalledFonts(): Promise<FontDescriptor[]> {
return await chrome.fontSettings.getFontList();
}
// Populate dropdown
async function populateFontDropdown(dropdownId: string): Promise<void> {
const fonts = await getInstalledFonts();
const dropdown = document.getElementById(dropdownId) as HTMLSelectElement;
fonts.sort((a, b) => a.displayName.localeCompare(b.displayName));
dropdown.innerHTML = '<option value="">-- Select Font --</option>';
for (const font of fonts) {
const option = document.createElement("option");
option.value = font.fontId;
option.textContent = font.displayName;
dropdown.appendChild(option);
}
}
Loading All Font Preferences
// storage/fontConfig.ts
import { createStorage, defineSchema } from "@theluckystrike/webext-storage";
const fontConfigSchema = defineSchema({
fontPreferences: {
type: "object",
default: { standard: "", sansserif: "", serif: "", fixed: "" },
},
minimumFontSize: { type: "number", default: 0 },
});
const fontStorage = createStorage(fontConfigSchema);
async function loadAllFontPreferences(): Promise<Record<string, string>> {
const families = ["standard", "sansserif", "serif", "fixed", "cursive", "fantasy"];
const preferences: Record<string, string> = {};
for (const family of families) {
try {
const font = await getCurrentFont(family);
preferences[family] = font.fontId;
} catch {
preferences[family] = "";
}
}
await fontStorage.set("fontPreferences", preferences);
return preferences;
}
Minimum Font Size
async function getMinimumFontSize(): Promise<number> {
const result = await chrome.fontSettings.getMinimumFontSize();
return result.pixelSize;
}
async function setMinimumFontSize(pixelSize: number): Promise<void> {
await chrome.fontSettings.setMinimumFontSize({ pixelSize });
}
Pattern 3: Font Override by Script/Language
Script codes: Jpan (Japanese), Hang (Korean), Hans (Simplified Chinese), Hant (Traditional Chinese), Arab (Arabic), Latn (Latin).
Setting Script-Specific Fonts
const SCRIPT_CODES = {
JAPANESE: "Jpan",
KOREAN: "Hang",
CHINESE_SIMPLIFIED: "Hans",
CHINESE_TRADITIONAL: "Hant",
} as const;
async function setJapaneseFont(fontId: string): Promise<void> {
await setFont("standard", fontId, SCRIPT_CODES.JAPANESE);
await setFont("sansserif", fontId, SCRIPT_CODES.JAPANESE);
}
async function configureMultilingualFonts(): Promise<void> {
await setJapaneseFont("Noto Sans JP");
await setFont("sansserif", "Noto Sans KR", SCRIPT_CODES.KOREAN);
await setFont("sansserif", "Noto Sans SC", SCRIPT_CODES.CHINESE_SIMPLIFIED);
await setFont("sansserif", "Noto Sans TC", SCRIPT_CODES.CHINESE_TRADITIONAL);
}
Getting Script Fonts
const AVAILABLE_SCRIPTS = [
{ code: "Latn", name: "Latin" },
{ code: "Jpan", name: "Japanese" },
{ code: "Hang", name: "Korean" },
{ code: "Hans", name: "Chinese Simplified" },
];
async function getAllScriptFonts(genericFamily: string): Promise<Map<string, string>> {
const fontMap = new Map<string, string>();
for (const script of AVAILABLE_SCRIPTS) {
try {
const font = await getCurrentFont(genericFamily, script.code);
fontMap.set(script.code, font.fontId);
} catch { /* ignore */ }
}
return fontMap;
}
Pattern 4: Font Preferences UI
Options Page HTML
<!-- options.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Font Settings</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<main class="options-container">
<h1>Font Preferences</h1>
<section>
<h2>Font Families</h2>
<div class="font-row">
<label for="font-standard">Standard:</label>
<select id="font-standard" data-family="standard"></select>
</div>
<div class="font-row">
<label for="font-sansserif">Sans-Serif:</label>
<select id="font-sansserif" data-family="sansserif"></select>
</div>
</section>
<section>
<h2>Live Preview</h2>
<div id="font-preview" class="preview-box">
<p>The quick brown fox jumps over the lazy dog.</p>
<p>ABCDEFGHIJKLMNOPQRSTUVWXYZ</p>
</div>
</section>
<section class="actions">
<button id="reset-defaults">Reset to Defaults</button>
<button id="save-settings">Save Settings</button>
</section>
</main>
<script src="options.js"></script>
</body>
</html>
Options Page TypeScript
// options.ts
import { fontStorage } from "./storage/fontConfig";
const FAMILIES = ["standard", "sansserif", "serif", "fixed", "cursive", "fantasy"] as const;
async function init(): Promise<void> {
const fonts = await getInstalledFonts();
populateDropdowns(fonts);
const prefs = await fontStorage.get();
applyPreferences(prefs.fontPreferences);
setupListeners();
}
function populateDropdowns(fonts: FontDescriptor[]): void {
for (const family of FAMILIES) {
const select = document.getElementById(`font-${family}`) as HTMLSelectElement;
select.innerHTML = '<option value="">System Default</option>';
for (const font of fonts.sort((a, b) => a.displayName.localeCompare(b.displayName))) {
const opt = document.createElement("option");
opt.value = font.fontId;
opt.textContent = font.displayName;
select.appendChild(opt);
}
}
}
function applyPreferences(fonts: Record<string, string>): void {
for (const [family, fontId] of Object.entries(fonts)) {
const select = document.getElementById(`font-${family}`) as HTMLSelectElement;
if (select && fontId) select.value = fontId;
}
updatePreview();
}
function setupListeners(): void {
document.getElementById("save-settings")?.addEventListener("click", saveSettings);
document.getElementById("reset-defaults")?.addEventListener("click", resetDefaults);
for (const family of FAMILIES) {
document.getElementById(`font-${family}`)?.addEventListener("change", updatePreview);
}
}
async function saveSettings(): Promise<void> {
const fonts: Record<string, string> = {};
for (const family of FAMILIES) {
const select = document.getElementById(`font-${family}`) as HTMLSelectElement;
fonts[family] = select?.value || "";
}
await fontStorage.set("fonts", fonts);
for (const [family, fontId] of Object.entries(fonts)) {
if (fontId) await setFont(family, fontId);
}
showStatus("Settings saved!", "success");
}
async function resetDefaults(): Promise<void> {
if (!confirm("Reset to system defaults?")) return;
await fontStorage.set("fonts", { standard: "", sansserif: "", serif: "", fixed: "", cursive: "", fantasy: "" });
const fonts = await getInstalledFonts();
populateDropdowns(fonts);
updatePreview();
showStatus("Reset complete", "success");
}
function updatePreview(): void {
const preview = document.getElementById("font-preview") as HTMLElement;
const standard = (document.getElementById("font-standard") as HTMLSelectElement)?.value;
preview.style.fontFamily = standard || "inherit";
}
function showStatus(message: string, type: string): void {
const status = document.getElementById("status-message");
if (status) {
status.textContent = message;
status.className = `status-${type}`;
}
}
document.addEventListener("DOMContentLoaded", init);
Pattern 5: Per-Site Font Override via Content Script
Storage Schema
// storage/siteFonts.ts
import { createStorage, defineSchema } from "@theluckystrike/webext-storage";
const siteFontsSchema = defineSchema({
siteFontRules: { type: "object", default: {} },
globalOverrideEnabled: { type: "boolean", default: false },
});
export const siteFontStorage = createStorage(siteFontsSchema);
export interface SiteFontRule {
domain: string;
fontFamily: string;
fontSize?: number;
enabled: boolean;
}
Content Script
// content-scripts/fontOverride.ts
import { siteFontStorage } from "../storage/siteFonts";
async function applySiteFontOverrides(): Promise<void> {
const { siteFontRules, globalOverrideEnabled } = await siteFontStorage.get();
if (!globalOverrideEnabled) return;
const domain = window.location.hostname;
const rules = Object.values(siteFontRules).filter(
(r: SiteFontRule) => r.enabled && domain.includes(r.domain)
);
for (const rule of rules) {
injectFontOverride(rule);
}
}
function injectFontOverride(rule: SiteFontRule): void {
const style = document.createElement("style");
style.id = "font-override-style";
const fontFamily = rule.fontFamily.includes(" ") ? `"${rule.fontFamily}"` : rule.fontFamily;
const fontSize = rule.fontSize ? `font-size: ${rule.fontSize}px !important;` : "";
style.textContent = `
body, body * { font-family: ${fontFamily} !important; ${fontSize} }
`;
document.head.appendChild(style);
}
function removeOverrides(): void {
document.getElementById("font-override-style")?.remove();
}
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "UPDATE_FONTS") {
removeOverrides().then(applySiteFontOverrides);
}
});
document.addEventListener("DOMContentLoaded", applySiteFontOverrides);
Custom Fonts with @font-face
function loadCustomFonts(): void {
const CUSTOM_FONTS = [
{ family: "MyFont", url: "fonts/MyFont-Regular.woff2", weight: "normal" },
{ family: "MyFont", url: "fonts/MyFont-Bold.woff2", weight: "bold" },
];
const style = document.createElement("style");
style.textContent = CUSTOM_FONTS.map(f =>
`@font-face { font-family: '${f.family}'; src: url('${f.url}'); font-weight: ${f.weight}; }`
).join("\n");
document.head.appendChild(style);
}
Pattern 6: Reading Mode with Custom Typography
Storage
// storage/readingMode.ts
import { createStorage, defineSchema } from "@theluckystrike/webext-storage";
const readingModeSchema = defineSchema({
enabled: { type: "boolean", default: false },
fontFamily: { type: "string", default: "Georgia" },
fontSize: { type: "number", default: 18 },
lineHeight: { type: "number", default: 1.6 },
maxWidth: { type: "number", default: 680 },
textColor: { type: "string", default: "#333333" },
backgroundColor: { type: "string", default: "#fafafa" },
});
export const readingModeStorage = createStorage(readingModeSchema);
Content Script
// content-scripts/readingMode.ts
import { readingModeStorage } from "../storage/readingMode";
let originalContent: string | null = null;
async function toggleReadingMode(): Promise<void> {
const settings = await readingModeStorage.get();
if (settings.enabled) {
disableReadingMode();
} else {
enableReadingMode(settings);
}
}
function enableReadingMode(settings: typeof readingModeSchema): void {
originalContent = document.body.innerHTML;
const container = document.createElement("div");
container.id = "reading-container";
container.style.cssText = `
max-width: ${settings.maxWidth}px; margin: 0 auto; padding: 40px 20px;
font-family: ${settings.fontFamily}, serif; font-size: ${settings.fontSize}px;
line-height: ${settings.lineHeight}; color: ${settings.textColor};
background: ${settings.backgroundColor};
`;
// Extract main content
const main = document.querySelector("article") || document.querySelector("main") || document.body;
const content = main.cloneNode(true) as Element;
content.querySelectorAll("script, style, nav, footer, aside").forEach(el => el.remove());
container.innerHTML = content.innerHTML;
document.body.innerHTML = "";
document.body.appendChild(container);
document.body.classList.add("reading-mode");
readingModeStorage.set("enabled", true);
}
function disableReadingMode(): void {
if (originalContent) {
document.body.innerHTML = originalContent;
document.body.classList.remove("reading-mode");
}
readingModeStorage.set("enabled", false);
}
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "TOGGLE_READING_MODE") toggleReadingMode();
});
// Init
readingModeStorage.get("enabled").then(enabled => {
if (enabled) readingModeStorage.get().then(enableReadingMode);
});
Background Script for Toggle
// background.ts
chrome.action.onClicked.addListener(async (tab) => {
if (tab.id) {
await chrome.tabs.sendMessage(tab.id, { type: "TOGGLE_READING_MODE" });
}
});
chrome.commands.onCommand.addListener(async (cmd) => {
if (cmd === "toggle-reading-mode") {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.id) await chrome.tabs.sendMessage(tab.id, { type: "TOGGLE_READING_MODE" });
}
});
Pattern 7: Font Change Monitoring
// background.ts / listeners/fontChangeListener.ts
import { fontStorage } from "../storage/fontConfig";
interface FontChangedEvent {
fontId: string;
genericFamily: string;
script?: string;
}
function initFontListeners(): void {
chrome.fontSettings.onFontChanged.addListener((event: FontChangedEvent) => {
console.log("Font changed:", event.genericFamily, "->", event.fontId);
fontStorage.get("fonts").then(prefs => {
const fonts = { ...prefs.fonts };
fonts[event.genericFamily] = event.fontId;
fontStorage.set("fonts", fonts);
});
notifyViews("FONT_CHANGED", { family: event.genericFamily, font: event.fontId });
});
chrome.fontSettings.onMinimumFontSizeChanged.addListener((event) => {
console.log("Min font size changed:", event.pixelSize);
fontStorage.set("minimumFontSize", event.pixelSize);
notifyViews("MIN_FONT_SIZE_CHANGED", { pixelSize: event.pixelSize });
});
}
function notifyViews(type: string, data: unknown): void {
chrome.runtime.sendMessage({ type, data }).catch(() => {});
}
// Fallback polling if listeners fail
let pollInterval: number | null = null;
function startPolling(): void {
if (pollInterval) return;
let lastFonts: Record<string, string> = {};
loadAllFontPreferences().then(f => { lastFonts = f; });
pollInterval = window.setInterval(async () => {
const current = await loadAllFontPreferences();
for (const [family, font] of Object.entries(current)) {
if (lastFonts[family] !== font) {
notifyViews("FONT_CHANGED", { family, font });
}
}
lastFonts = current;
}, 5000);
}
Pattern 8: Accessibility Font Patterns
Dyslexia-Friendly Mode
// content-scripts/dyslexiaFont.ts
import { createStorage, defineSchema } from "@theluckystrike/webext-storage";
const dyslexiaSchema = defineSchema({
enabled: { type: "boolean", default: false },
fontSize: { type: "number", default: 18 },
letterSpacing: { type: "number", default: 0.1 },
});
export const dyslexiaStorage = createStorage(dyslexiaSchema);
const DYSLEXIA_FONTS = ["OpenDyslexic", "Comic Sans MS", "Arial"];
async function applyDyslexiaMode(): Promise<void> {
const s = await dyslexiaStorage.get();
if (!s.enabled) return;
const style = document.createElement("style");
style.id = "dyslexia-style";
const fonts = DYSLEXIA_FONTS.map(f => `'${f}'`).join(", ");
style.textContent = `
body, body * { font-family: ${fonts}, sans-serif !important; }
body { font-size: ${s.fontSize}px !important; letter-spacing: ${s.letterSpacing}em !important; line-height: 1.8 !important; }
p, li { max-width: 60ch !important; }
`;
document.head.appendChild(style);
}
function removeDyslexiaMode(): void {
document.getElementById("dyslexia-style")?.remove();
}
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "TOGGLE_DYSLEXIA") {
document.getElementById("dyslexia-style") ? removeDyslexiaMode() : applyDyslexiaMode();
}
});
High Readability Mode
// content-scripts/highReadability.ts
const highReadabilitySchema = defineSchema({
enabled: { type: "boolean", default: false },
fontSize: { type: "number", default: 20 },
lineHeight: { type: "number", default: 2.0 },
contrast: { type: "string", default: "high" },
});
export const highReadabilityStorage = createStorage(highReadabilitySchema);
const THEMES = {
normal: { bg: "#fafafa", text: "#333" },
high: { bg: "#fff", text: "#000" },
maximum: { bg: "#ffffcc", text: "#000" },
};
async function applyHighReadability(): Promise<void> {
const s = await highReadabilityStorage.get();
if (!s.enabled) return;
const theme = THEMES[s.contrast as keyof typeof THEMES] || THEMES.high;
const style = document.createElement("style");
style.id = "high-readability";
style.textContent = `
body { background: ${theme.bg} !important; color: ${theme.text} !important; }
body > * { max-width: 800px; margin: 0 auto; }
body { font-size: ${s.fontSize}px !important; line-height: ${s.lineHeight} !important; }
`;
document.head.appendChild(style);
}
System Preference Detection
function getSystemPreferences() {
return {
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
highContrast: window.matchMedia("(prefers-contrast: more)").matches,
darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches,
};
}
// Listen for changes
window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", e => {
console.log("Reduced motion:", e.matches);
});
Summary Table
| Pattern | Use Case | Key APIs |
|---|---|---|
| 1. Basics | Read/write font preferences | getFont(), setFont() |
| 2. Configuration | Enumerate fonts, system defaults | getFontList(), getMinimumFontSize() |
| 3. Script Override | CJK/multilingual support | Script-specific setFont() |
| 4. Preferences UI | Options page with preview | Storage + UI handlers |
| 5. Per-Site Override | Domain-specific fonts | Content script + CSS |
| 6. Reading Mode | Distraction-free reading | Typography settings |
| 7. Change Monitoring | Real-time sync | onFontChanged, onMinimumFontSizeChanged |
| 8. Accessibility | Dyslexia, high readability | Font injection + system prefs |
Quick Reference
// Permission: "fontSettings"
// Get/set font
const font = await chrome.fontSettings.getFont({ genericFamily: "sansserif" });
await chrome.fontSettings.setFont({ genericFamily: "sansserif", fontId: "Arial" });
// All fonts
const fonts = await chrome.fontSettings.getFontList();
// Min font size
const minSize = (await chrome.fontSettings.getMinimumFontSize()).pixelSize;
// Events
chrome.fontSettings.onFontChanged.addListener(e => {});
chrome.fontSettings.onMinimumFontSizeChanged.addListener(e => {});
Best Practices
- Request only necessary permissions —
fontSettingstriggers a warning - Cache preferences with
@theluckystrike/webext-storagefor performance - Handle errors gracefully — fonts may not exist on all systems
- Respect user preferences — don’t override without consent
- Test multilingual scripts thoroughly
- Provide accessibility options — dyslexia mode, high contrast -e —
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.