Chrome Extension Popup Patterns — Developer Guide

9 min read

Popup Patterns

Overview

The popup is the most common UI surface for Chrome extensions. It opens when the user clicks the toolbar icon and closes when they click away. This guide covers patterns for building effective popups with the @theluckystrike/webext-* toolkit.

Manifest Setup

{
  "action": {
    "default_popup": "popup.html",
    "default_icon": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
  }
}

Pattern 1: Display Data from Background

Popup requests data from background service worker:

// shared/messages.ts
type Messages = {
  getStats: { request: void; response: { blocked: number; allowed: number; lastUpdate: number } };
  getRecentItems: { request: { limit: number }; response: Array<{ title: string; url: string }> };
};

// popup.ts
import { createMessenger } from "@theluckystrike/webext-messaging";
const msg = createMessenger<Messages>();

async function render() {
  const stats = await msg.send("getStats", undefined);
  document.getElementById("blocked")!.textContent = String(stats.blocked);
  document.getElementById("allowed")!.textContent = String(stats.allowed);

  const items = await msg.send("getRecentItems", { limit: 10 });
  const list = document.getElementById("items")!;
  list.innerHTML = items.map(i => `<li><a href="${i.url}">${i.title}</a></li>`).join("");
}

render();

Pattern 2: Quick Settings Toggle

Toggle a feature on/off from popup, persisted in storage:

import { defineSchema, createStorage } from "@theluckystrike/webext-storage";
import { createMessenger } from "@theluckystrike/webext-messaging";

const schema = defineSchema({ enabled: true, blockCount: 0 });
const storage = createStorage({ schema });

type Messages = {
  toggleFeature: { request: { enabled: boolean }; response: { enabled: boolean } };
};
const msg = createMessenger<Messages>();

async function init() {
  const enabled = await storage.get("enabled");
  updateToggleUI(enabled);

  document.getElementById("toggle")?.addEventListener("click", async () => {
    const current = await storage.get("enabled");
    const newState = !current;
    await storage.set("enabled", newState);
    await msg.send("toggleFeature", { enabled: newState });
    updateToggleUI(newState);
  });
}

Pattern 3: Current Tab Context

Show info about the active tab:

async function showCurrentTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  document.getElementById("tab-title")!.textContent = tab.title ?? "Unknown";
  document.getElementById("tab-url")!.textContent = tab.url ?? "";
}

Pattern 4: Action Buttons (Run on Current Page)

import { createMessenger } from "@theluckystrike/webext-messaging";

type Messages = {
  extractData: { request: { tabId: number }; response: { wordCount: number; links: number } };
  injectCSS: { request: { tabId: number; theme: string }; response: { success: boolean } };
};
const msg = createMessenger<Messages>();

document.getElementById("extract")?.addEventListener("click", async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab.id) return;
  const data = await msg.send("extractData", { tabId: tab.id });
  document.getElementById("results")!.textContent = `${data.wordCount} words, ${data.links} links`;
});

Pattern 5: Form Input with Storage

import { defineSchema, createStorage } from "@theluckystrike/webext-storage";

const schema = defineSchema({
  quickNote: "",
  savedNotes: [] as Array<{ text: string; timestamp: number }>,
});
const storage = createStorage({ schema });

// Load last note
const quickNote = await storage.get("quickNote");
(document.getElementById("note") as HTMLTextAreaElement).value = quickNote;

// Auto-save as user types
document.getElementById("note")?.addEventListener("input", async (e) => {
  await storage.set("quickNote", (e.target as HTMLTextAreaElement).value);
});

// Save button
document.getElementById("save-note")?.addEventListener("click", async () => {
  const text = await storage.get("quickNote");
  if (!text.trim()) return;
  const notes = await storage.get("savedNotes");
  notes.push({ text, timestamp: Date.now() });
  await storage.setMany({ savedNotes: notes, quickNote: "" });
  (document.getElementById("note") as HTMLTextAreaElement).value = "";
  renderNotesList(notes);
});

Pattern 6: Loading States and Error Handling

import { MessagingError } from "@theluckystrike/webext-messaging";

async function loadData() {
  const loading = document.getElementById("loading")!;
  const content = document.getElementById("content")!;
  const error = document.getElementById("error")!;

  loading.style.display = "block";
  content.style.display = "none";
  error.style.display = "none";

  try {
    const data = await msg.send("getData", {});
    content.style.display = "block";
    renderData(data);
  } catch (err) {
    error.style.display = "block";
    if (err instanceof MessagingError) {
      error.textContent = "Could not connect to background service.";
    } else {
      error.textContent = "An unexpected error occurred.";
    }
  } finally {
    loading.style.display = "none";
  }
}

Pattern 7: Badge Updates from Popup

// Update badge after action
document.getElementById("mark-read")?.addEventListener("click", async () => {
  await msg.send("markAllRead", undefined);
  chrome.action.setBadgeText({ text: "" });
});

Complete popup.html Template

Provide a minimal but complete template:

Gotchas

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.