Build a Font Changer Chrome Extension: Customize Website Typography

23 min read

Build a Font Changer Chrome Extension: Customize Website Typography

Build a Font Changer Chrome Extension: Customize Website Typography

Have you ever visited a website and wished you could change the font to something easier to read? Maybe the text is too small, the font family doesn’t appeal to you, or you prefer reading in a specific typeface. A font changer Chrome extension solves this problem by allowing users to customize the typography of any website they visit. In this comprehensive guide, we’ll walk through building a complete font changer extension from scratch using Manifest V3.

Chrome extensions have become essential tools for personalizing the browsing experience. While there are many extensions available for blocking ads, managing tabs, and boosting productivity, font customization remains a niche with significant demand. Users with visual impairments, dyslexia, or personal preferences often struggle with website typography that doesn’t suit their needs. Building a font changer extension not only addresses these real problems but also teaches you valuable skills in Chrome extension development, including content script injection, storage API usage, and user interface design.

In this tutorial, you’ll learn how to create a fully functional font changer extension that can modify fonts on any website. We’ll cover everything from setting up the project structure to implementing the core functionality, adding a user-friendly popup interface, and finally publishing your extension to the Chrome Web Store.


Understanding the Font Changer Extension Architecture

Before diving into code, it’s essential to understand how a font changer extension works at a fundamental level. The core concept involves injecting custom CSS into web pages to override existing font styles. This process requires careful consideration of CSS specificity, font loading, and user preferences persistence.

The architecture of our font changer extension consists of three main components working together. First, the manifest file defines the extension’s configuration and permissions. Second, the popup interface provides users with controls to select and apply fonts. Third, the content script handles the actual CSS injection and font application on web pages. Additionally, we’ll use the Chrome Storage API to save user preferences so that selected fonts persist across browser sessions and across different websites.

The key challenge in building a font changer is ensuring that our custom fonts override existing styles without breaking website functionality. We’ll need to use aggressive CSS selectors with high specificity, handle web fonts properly, and provide fallback fonts for reliability. Another consideration is performance—applying font changes should be instant and not cause visible page layout shifts.


Project Setup and Manifest Configuration

Every Chrome extension begins with a manifest.json file. For our font changer extension, we’ll use Manifest V3, which is Google’s current standard. Let’s create the project structure and manifest file.

Create a new folder for your extension project and add the following manifest.json file:

{
  "manifest_version": 3,
  "name": "Font Changer - Custom Typography",
  "version": "1.0.0",
  "description": "Change fonts on any website. Customize typography for better readability and personal style.",
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["styles/injected.css"],
      "run_at": "document_end"
    }
  ],
  "background": {
    "service_worker": "background.js"
  }
}

The manifest configuration includes several critical elements worth explaining. The host_permissions with “" allows our extension to work on any website. The content_scripts section specifies that our content script should run on all pages after the document loads. We also include the storage permission to save user preferences and scripting permission for dynamic CSS injection.


The popup is what users see when they click the extension icon in their browser toolbar. We’ll create an intuitive interface that allows users to select fonts, adjust sizes, and apply their choices. Let’s build the popup HTML and JavaScript.

First, create popup.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Font Changer</title>
  <link rel="stylesheet" href="popup.css">
  <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&family=Roboto:wght@400;500&family=Lato:wght@400;700&family=Montserrat:wght@400;600&family=Playfair+Display:wght@400;700&family=Merriweather:wght@400;700&family=Source+Code+Pro:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
  <div class="container">
    <header>
      <h1>Font Changer</h1>
      <p class="subtitle">Customize website typography</p>
    </header>

    <div class="controls">
      <div class="control-group">
        <label for="fontFamily">Font Family</label>
        <select id="fontFamily">
          <option value="default">Default Font</option>
          <option value="Open Sans">Open Sans</option>
          <option value="Roboto">Roboto</option>
          <option value="Lato">Lato</option>
          <option value="Montserrat">Montserrat</option>
          <option value="Playfair Display">Playfair Display</option>
          <option value="Merriweather">Merriweather</option>
          <option value="Source Code Pro">Source Code Pro</option>
          <option value="Arial">Arial</option>
          <option value="Georgia">Georgia</option>
          <option value="Times New Roman">Times New Roman</option>
          <option value="Verdana">Verdana</option>
        </select>
      </div>

      <div class="control-group">
        <label for="fontSize">Base Font Size</label>
        <div class="slider-container">
          <input type="range" id="fontSize" min="12" max="24" value="16">
          <span id="fontSizeValue">16px</span>
        </div>
      </div>

      <div class="control-group">
        <label for="lineHeight">Line Height</label>
        <div class="slider-container">
          <input type="range" id="lineHeight" min="1" max="2.5" step="0.1" value="1.5">
          <span id="lineHeightValue">1.5</span>
        </div>
      </div>

      <div class="control-group">
        <label for="fontWeight">Font Weight</label>
        <select id="fontWeight">
          <option value="normal">Normal</option>
          <option value="bold">Bold</option>
          <option value="lighter">Light</option>
          <option value="300">300 - Light</option>
          <option value="400">400 - Regular</option>
          <option value="500">500 - Medium</option>
          <option value="600">600 - Semi Bold</option>
          <option value="700">700 - Bold</option>
        </select>
      </div>

      <div class="control-group checkbox-group">
        <label>
          <input type="checkbox" id="enableExtension">
          <span>Enable Font Changes</span>
        </label>
      </div>
    </div>

    <div class="actions">
      <button id="applyBtn" class="primary-btn">Apply</button>
      <button id="resetBtn" class="secondary-btn">Reset</button>
    </div>

    <footer>
      <p class="status" id="status"></p>
    </footer>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Now let’s create the popup.css to style our interface:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  width: 320px;
  background: #ffffff;
  color: #333;
}

.container {
  padding: 20px;
}

header {
  text-align: center;
  margin-bottom: 20px;
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 15px;
}

h1 {
  font-size: 20px;
  font-weight: 600;
  color: #1a73e8;
}

.subtitle {
  font-size: 12px;
  color: #666;
  margin-top: 4px;
}

.controls {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.control-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.control-group label {
  font-size: 13px;
  font-weight: 500;
  color: #444;
}

select, input[type="range"] {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
}

select:focus, input:focus {
  outline: none;
  border-color: #1a73e8;
  box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}

.slider-container {
  display: flex;
  align-items: center;
  gap: 10px;
}

.slider-container input[type="range"] {
  flex: 1;
}

.slider-container span {
  min-width: 40px;
  text-align: right;
  font-size: 13px;
  color: #666;
}

.checkbox-group label {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.checkbox-group input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

.actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

button {
  flex: 1;
  padding: 10px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s, transform 0.1s;
}

button:hover {
  transform: translateY(-1px);
}

button:active {
  transform: translateY(0);
}

.primary-btn {
  background: #1a73e8;
  color: white;
}

.primary-btn:hover {
  background: #1557b0;
}

.secondary-btn {
  background: #f1f3f4;
  color: #444;
}

.secondary-btn:hover {
  background: #e8eaed;
}

footer {
  margin-top: 15px;
  text-align: center;
}

.status {
  font-size: 12px;
  color: #34a853;
  min-height: 18px;
}

.status.error {
  color: #ea4335;
}

The popup interface includes several controls for customizing typography. Users can select from predefined font families, adjust font size with a slider, modify line height, choose font weight, and toggle the extension on or off. We’ve included popular Google Fonts as options, along with standard system fonts.


Now let’s create the popup.js file that handles user interactions and communicates with the rest of the extension:

document.addEventListener('DOMContentLoaded', () => {
  const fontFamilySelect = document.getElementById('fontFamily');
  const fontSizeSlider = document.getElementById('fontSize');
  const fontSizeValue = document.getElementById('fontSizeValue');
  const lineHeightSlider = document.getElementById('lineHeight');
  const lineHeightValue = document.getElementById('lineHeightValue');
  const fontWeightSelect = document.getElementById('fontWeight');
  const enableCheckbox = document.getElementById('enableExtension');
  const applyBtn = document.getElementById('applyBtn');
  const resetBtn = document.getElementById('resetBtn');
  const statusEl = document.getElementById('status');

  // Load saved settings
  loadSettings();

  // Event listeners
  fontSizeSlider.addEventListener('input', () => {
    fontSizeValue.textContent = fontSizeSlider.value + 'px';
  });

  lineHeightSlider.addEventListener('input', () => {
    lineHeightValue.textContent = lineHeightSlider.value;
  });

  applyBtn.addEventListener('click', applySettings);
  resetBtn.addEventListener('click', resetSettings);

  function loadSettings() {
    chrome.storage.sync.get([
      'fontFamily',
      'fontSize',
      'lineHeight',
      'fontWeight',
      'enabled'
    ], (settings) => {
      if (settings.fontFamily) {
        fontFamilySelect.value = settings.fontFamily;
      }
      if (settings.fontSize) {
        fontSizeSlider.value = settings.fontSize;
        fontSizeValue.textContent = settings.fontSize + 'px';
      }
      if (settings.lineHeight) {
        lineHeightSlider.value = settings.lineHeight;
        lineHeightValue.textContent = settings.lineHeight;
      }
      if (settings.fontWeight) {
        fontWeightSelect.value = settings.fontWeight;
      }
      enableCheckbox.checked = settings.enabled !== false;
    });
  }

  function applySettings() {
    const settings = {
      fontFamily: fontFamilySelect.value,
      fontSize: parseInt(fontSizeSlider.value),
      lineHeight: parseFloat(lineHeightSlider.value),
      fontWeight: fontWeightSelect.value,
      enabled: enableCheckbox.checked
    };

    // Save to storage
    chrome.storage.sync.set(settings, () => {
      // Apply to current tab
      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        if (tabs[0]) {
          chrome.tabs.sendMessage(tabs[0].id, {
            action: 'applyFontSettings',
            settings: settings
          }, (response) => {
            if (chrome.runtime.lastError) {
              showStatus('Cannot apply to this page', true);
            } else {
              showStatus('Font settings applied!');
            }
          });
        }
      });
    });
  }

  function resetSettings() {
    const defaultSettings = {
      fontFamily: 'default',
      fontSize: 16,
      lineHeight: 1.5,
      fontWeight: 'normal',
      enabled: false
    };

    chrome.storage.sync.set(defaultSettings, () => {
      fontFamilySelect.value = 'default';
      fontSizeSlider.value = 16;
      fontSizeValue.textContent = '16px';
      lineHeightSlider.value = 1.5;
      lineHeightValue.textContent = '1.5';
      fontWeightSelect.value = 'normal';
      enableCheckbox.checked = false;

      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        if (tabs[0]) {
          chrome.tabs.sendMessage(tabs[0].id, {
            action: 'resetFontSettings'
          });
        }
      });

      showStatus('Settings reset to default');
    });
  }

  function showStatus(message, isError = false) {
    statusEl.textContent = message;
    statusEl.className = 'status' + (isError ? ' error' : '');
    setTimeout(() => {
      statusEl.textContent = '';
    }, 3000);
  }
});

This JavaScript handles loading and saving user preferences using the Chrome Storage API. When the popup opens, it retrieves saved settings and populates the form controls. When users click Apply, it saves the settings to storage and sends a message to the active tab’s content script to apply the font changes. The Reset button restores default settings and removes custom fonts from the page.


Creating the Content Script

The content script is the component that actually modifies page fonts. It runs in the context of web pages and applies the CSS changes. Let’s create content.js:

// Listen for messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'applyFontSettings') {
    applyFontSettings(message.settings);
    sendResponse({ success: true });
  } else if (message.action === 'resetFontSettings') {
    resetFontSettings();
    sendResponse({ success: true });
  }
  return true;
});

// Check for saved settings on page load
chrome.storage.sync.get(['enabled', 'fontFamily', 'fontSize', 'lineHeight', 'fontWeight'], (settings) => {
  if (settings.enabled && settings.fontFamily && settings.fontFamily !== 'default') {
    applyFontSettings(settings);
  }
});

function applyFontSettings(settings) {
  // Remove existing style if any
  const existingStyle = document.getElementById('font-changer-styles');
  if (existingStyle) {
    existingStyle.remove();
  }

  if (!settings.enabled || settings.fontFamily === 'default') {
    return;
  }

  // Create style element
  const style = document.createElement('style');
  style.id = 'font-changer-styles';

  // Build CSS with high specificity
  let css = `
    html body,
    html .content,
    html article,
    html section,
    html main,
    html p,
    html span,
    html div,
    html a,
    html li,
    html td,
    html th,
    html label,
    html h1,
    html h2,
    html h3,
    html h4,
    html h5,
    html h6,
    html input,
    html textarea,
    html select,
    html button {
  `;

  // Add font family
  if (settings.fontFamily && settings.fontFamily !== 'default') {
    css += `font-family: '${settings.fontFamily}', sans-serif !important;\n`;
  }

  // Add font size
  if (settings.fontSize) {
    css += `font-size: ${settings.fontSize}px !important;\n`;
  }

  // Add line height
  if (settings.lineHeight) {
    css += `line-height: ${settings.lineHeight} !important;\n`;
  }

  // Add font weight
  if (settings.fontWeight && settings.fontWeight !== 'normal') {
    css += `font-weight: ${settings.fontWeight} !important;\n`;
  }

  css += `}`;

  // Apply to specific text elements for better targeting
  css += `
    html body p,
    html body li,
    html body td,
    html body th,
    html body span:not(.icon):not([class*="icon"]),
    html body div:not(.icon):not([class*="icon"]) {
      font-family: inherit;
    }
  `;

  style.textContent = css;
  document.head.appendChild(style);
}

function resetFontSettings() {
  const existingStyle = document.getElementById('font-changer-styles');
  if (existingStyle) {
    existingStyle.remove();
  }
}

The content script applies font changes by injecting a style element with high-specificity CSS selectors. We use the !important flag to override existing inline styles and ensure our custom fonts take precedence. The script listens for messages from the popup and applies or removes font settings accordingly.


Creating the Background Service Worker

The background service worker handles extension lifecycle events and can be used for more advanced features. For this basic implementation, we’ll create a minimal background.js:

// Background service worker for Font Changer extension

// Handle extension installation
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    console.log('Font Changer extension installed');
    
    // Set default settings
    chrome.storage.sync.set({
      fontFamily: 'default',
      fontSize: 16,
      lineHeight: 1.5,
      fontWeight: 'normal',
      enabled: false
    });
  }
});

// Handle extension icon click (if popup not defined)
chrome.action.onClicked.addListener((tab) => {
  // This won't fire because we have a popup defined
  // But useful to know for extensions without popup
});

Adding Extension Icons

Every extension needs icons. Create an icons folder and add placeholder icons. For development, you can create simple colored squares using any image editor. The manifest references three icon sizes: 16x16, 48x48, and 128x128 pixels.


Testing Your Extension

Now that we’ve created all the components, let’s test the extension in Chrome:

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” using the toggle in the top right corner
  3. Click “Load unpacked” and select your extension folder
  4. The extension icon should appear in your browser toolbar
  5. Visit any website and click the extension icon
  6. Select a font family, adjust settings, and click Apply
  7. The website’s typography should change according to your settings

If the fonts don’t appear correctly, check the extension’s console for errors. You can do this by going to chrome://extensions/, finding your extension, and clicking “service worker” or “inspect views” to open the developer tools.


Advanced Features to Consider

Once you have the basic font changer working, consider adding these advanced features:

Custom Font Upload

Allow users to upload their own TrueType or OpenType font files for use on any website. This requires additional handling for font loading and might need to handle licensing considerations.

Per-Site Settings

Store different font preferences for different websites. Users might prefer reading news sites in one font and coding documentation in another.

Font Preview

Add a live preview in the popup that shows what the selected font looks like before applying it to the actual page.

Preset Themes

Create preset font combinations for common use cases, such as “Dyslexia Friendly” with OpenDyslexic font, larger sizes, and increased line spacing.

Dark Mode Integration

Automatically adjust font colors or backgrounds when users enable dark mode on websites.


Publishing Your Extension

When you’re ready to share your extension with the world, follow these steps to publish to the Chrome Web Store:

  1. Create a developer account at the Chrome Web Store Developer Dashboard
  2. Package your extension as a ZIP file (exclude unnecessary files)
  3. Upload your extension and fill in the store listing details
  4. Add screenshots, a detailed description, and appropriate categories
  5. Submit for review (usually takes a few hours to a few days)
  6. Once approved, your extension will be available for installation

Make sure your extension follows Google’s policies, including proper disclosure of permissions and privacy practices.


Conclusion

Congratulations! You’ve successfully built a complete font changer Chrome extension. This project demonstrates several important concepts in extension development, including Manifest V3 configuration, content script injection, popup UI design, storage API usage, and cross-component communication.

The font changer extension you created can be extended in numerous ways. You could add support for custom fonts, create preset themes, implement per-site settings, or even build a social feature where users can share their favorite font combinations. The Chrome extension platform provides powerful APIs that enable all sorts of creative projects.

Building Chrome extensions is an excellent way to learn web development skills while creating useful tools that can improve people’s browsing experiences. The font changer extension addresses a genuine need—many users struggle with website typography for reasons ranging from visual impairments to simple personal preferences. Your extension gives them control over how they experience the web.

Remember to test your extension thoroughly across different websites and browsers, gather user feedback, and continuously improve based on real-world usage. With the foundation you’ve built in this guide, you’re well on your way to creating polished, professional Chrome extensions.

No previous article
No next article