Claude Skills Guide

Building a Chrome Extension Text Expander from Scratch

Text expanders save you from typing repetitive phrases, code snippets, and formatted text. Instead of typing your full email address, a common code comment, or a lengthy signature every time, you type a short trigger and the extension expands it. Building this functionality as a Chrome extension gives you cross-site support, cloud sync possibilities, and integration with your browser workflow.

This guide walks you through creating a functional text expander extension. You’ll learn the architecture, see working code, and understand how to customize it for your needs.

How a Chrome Text Expander Works

A text expander monitors your keystrokes in input fields across the web. When you type a trigger sequence—typically a short prefix like ;sig or ;;email—the extension replaces that trigger with your predefined expansion. The replacement happens at the browser level, making it work in text boxes, textareas, code editors, and any other editable field.

The core components you need:

  1. Manifest file - Defines permissions and extension structure
  2. Content script - Injects into web pages to capture keystrokes
  3. Background script - Handles storage and long-term data management
  4. Popup interface - Lets users manage their snippets

Setting Up Your Extension

Create a new folder for your project and add the manifest file first:

{
  "manifest_version": 3,
  "name": "CodeExpander",
  "version": "1.0",
  "description": "Custom text expander for developers",
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["<all_urls>"],
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"],
    "run_at": "document_start"
  }],
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  }
}

The activeTab permission lets you access the current page when needed, while <all_urls> in host_permissions allows your content script to run everywhere. The run_at: "_start" setting ensures your script loads before page content, giving you early access to input fields.

Capturing Keystrokes in Content Scripts

The content script listens for keyboard input and checks each keystroke against your snippet definitions. Here’s a working implementation:

// content.js
class TextExpander {
  constructor() {
    this.snippets = {};
    this.buffer = '';
    this.maxBufferLength = 20;
    this.loadSnippets();
  }

  async loadSnippets() {
    const result = await chrome.storage.sync.get('snippets');
    this.snippets = result.snippets || {};
  }

  handleKeydown(event) {
    if (event.ctrlKey || event.altKey || event.metaKey) {
      this.buffer = '';
      return;
    }

    if (event.key === 'Backspace') {
      this.buffer = this.buffer.slice(0, -1);
      return;
    }

    if (event.key.length === 1) {
      this.buffer += event.key;
      
      if (this.buffer.length > this.maxBufferLength) {
        this.buffer = this.buffer.slice(-this.maxBufferLength);
      }

      this.checkForExpansion(event);
    } else if (event.key === 'Escape') {
      this.buffer = '';
    }
  }

  checkForExpansion(event) {
    for (const [trigger, expansion] of Object.entries(this.snippets)) {
      if (this.buffer.endsWith(trigger)) {
        event.preventDefault();
        this.expandText(trigger, expansion);
        break;
      }
    }
  }

  expandText(trigger, expansion) {
    const activeElement = document.activeElement;
    if (!activeElement || !this.isEditable(activeElement)) return;

    const start = activeElement.selectionStart - trigger.length;
    const end = activeElement.selectionEnd;
    const text = activeElement.value || activeElement.textContent;

    const newText = text.substring(0, start) + expansion + text.substring(end);
    
    if (activeElement.value !== undefined) {
      activeElement.value = newText;
    } else {
      activeElement.textContent = newText;
    }

    const cursorPos = start + expansion.length;
    activeElement.setSelectionRange(cursorPos, cursorPos);
    
    this.buffer = '';
    
    // Dispatch input event for React and other frameworks
    activeElement.dispatchEvent(new Event('input', { bubbles: true }));
  }

  isEditable(element) {
    const tagName = element.tagName.toLowerCase();
    return (tagName === 'input' && element.type === 'text') ||
           tagName === 'textarea' ||
           element.isContentEditable;
  }
}

const expander = new TextExpander();
document.addEventListener('keydown', (e) => expander.handleKeydown(e));

This script maintains a rolling buffer of recent keystrokes. When the buffer matches a trigger, it calculates the cursor position, removes the trigger, and inserts the full expansion. The dispatchEvent call ensures React and similar frameworks update their internal state.

Managing Snippets with Chrome Storage

Users need a way to add, edit, and delete snippets. Create a simple popup interface:

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 320px; font-family: system-ui; padding: 16px; }
    h2 { margin: 0 0 16px; font-size: 16px; }
    .snippet-form { display: flex; flex-direction: column; gap: 8px; }
    input, textarea { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
    button { padding: 8px 16px; background: #4285f4; color: white; 
             border: none; border-radius: 4px; cursor: pointer; }
    .snippet-list { margin-top: 16px; border-top: 1px solid #eee; }
    .snippet-item { padding: 8px 0; border-bottom: 1px solid #eee; 
                    display: flex; justify-content: space-between; }
    .trigger { color: #4285f4; font-weight: bold; }
  </style>
</head>
<body>
  <h2>CodeExpander Snippets</h2>
  <div class="snippet-form">
    <input type="text" id="trigger" placeholder="Trigger (e.g., ;sig)">
    <textarea id="expansion" placeholder="Expansion text..." rows="3"></textarea>
    <button id="save">Save Snippet</button>
  </div>
  <div class="snippet-list" id="snippetList"></div>
  <script src="popup.js"></script>
</body>
</html>

The popup JavaScript handles loading and saving:

// popup.js
document.addEventListener('DOMContentLoaded', () => {
  loadSnippets();
  
  document.getElementById('save').addEventListener('click', async () => {
    const trigger = document.getElementById('trigger').value;
    const expansion = document.getElementById('expansion').value;
    
    if (!trigger || !expansion) return;

    const result = await chrome.storage.sync.get('snippets');
    const snippets = result.snippets || {};
    snippets[trigger] = expansion;
    
    await chrome.storage.sync.set({ snippets });
    
    document.getElementById('trigger').value = '';
    document.getElementById('expansion').value = '';
    loadSnippets();
  });
});

async function loadSnippets() {
  const result = await chrome.storage.sync.get('snippets');
  const snippets = result.snippets || {};
  const list = document.getElementById('snippetList');
  
  list.innerHTML = Object.entries(snippets)
    .map(([trigger, expansion]) => `
      <div class="snippet-item">
        <span><span class="trigger">${trigger}</span> → ${expansion.substring(0, 30)}...</span>
      </div>
    `).join('');
}

Chrome’s storage.sync automatically syncs your snippets across devices when the user signs into Chrome. This gives you cloud sync without additional infrastructure.

Practical Use Cases for Developers

Once you have a working text expander, here are practical applications:

Code snippets: Store common patterns like console logging, React component templates, or import statements. A trigger like ;clg expands to console.log('', ); with your cursor positioned after the first quote.

Email templates: Create shortcuts for frequently sent messages—meeting requests, status updates, or support responses.

Documentation shortcuts: Expand :api to your API endpoint documentation link, or :contrib to your contribution guidelines.

Date and time stamps: Create triggers that insert dynamic content. You can store a placeholder like {{date}} and use a content script replacement to insert today’s date when the expansion occurs.

Advanced Features to Consider

As you extend your implementation, consider adding:

Building your own text expander gives you full control over triggers, expansions, and storage. You can tailor it exactly to your workflow without relying on third-party services.


Built by theluckystrike — More at zovo.one