~/browser-erweiterungen/chrome-erweiterung-programmieren/

Chrome-Erweiterung programmieren: Schritt für Schritt mit Manifest V3

Von der leeren manifest.json bis zur funktionierenden Erweiterung mit Popup, Service Worker und Kontextmenü: ein vollständiges Beispiel zum Nachbauen.

Eine Chrome-Erweiterung programmieren Sie mit einer Datei manifest.json im Format Manifest V3 und ein paar HTML- und JavaScript-Dateien. Den Ordner laden Sie unter chrome://extensions im Entwicklermodus, und die Erweiterung läuft sofort. Diese Anleitung baut ein vollständiges, kleines Beispiel: einen Lesezeit-Schätzer mit Popup, Service Worker und Kontextmenü.

Was Sie brauchen

  • Einen aktuellen Chrome oder einen anderen Chromium-Browser wie Edge oder Brave.
  • Einen Texteditor, zum Beispiel VS Code.
  • Grundkenntnisse in JavaScript, insbesondere async und await.

Ein Build-Werkzeug ist für den Anfang nicht nötig. Sobald das Projekt wächst, lohnt sich ein Setup mit TypeScript und Bundler, das im Bereich Programmiergrundlagen beschrieben ist.

Das Beispielprojekt

Die Erweiterung soll zwei Dinge können:

  1. Ein Klick auf das Symbol öffnet ein Popup, das die Wörter der aktuellen Seite zählt und eine geschätzte Lesezeit anzeigt.
  2. Ein Rechtsklick auf markierten Text bietet den Eintrag „Wörter in Auswahl zählen“ an. Das Ergebnis erscheint als Zahl auf dem Symbol.

Die Ordnerstruktur sieht am Ende so aus:

lesezeit/
├── manifest.json
├── background.js
├── popup.html
├── popup.css
├── popup.js
└── icons/
    ├── 16.png
    ├── 48.png
    └── 128.png

Schritt 1: die manifest.json

Das Manifest ist die einzige Pflichtdatei. Es nennt die Bestandteile und die Berechtigungen.

{
  "manifest_version": 3,
  "name": "Lesezeit-Schätzer",
  "version": "1.0.0",
  "description": "Schätzt die Lesezeit der aktuellen Seite und zählt Wörter in markiertem Text.",
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" },
  "action": {
    "default_title": "Lesezeit schätzen",
    "default_popup": "popup.html"
  },
  "background": { "service_worker": "background.js" },
  "permissions": ["activeTab", "scripting", "contextMenus", "storage"]
}

Zu den Feldern:

  • manifest_version muss 3 sein. Chrome lädt keine Erweiterungen nach Manifest V2 mehr. Was sich gegenüber der alten Version geändert hat, erklärt der Beitrag Manifest V2 vs. V3.
  • version besteht aus bis zu vier durch Punkte getrennten Zahlen und muss bei jedem Store-Update steigen.
  • action steht für das Symbol in der Symbolleiste. default_popup legt fest, welche Seite beim Klick erscheint.
  • permissions enthält nur, was wirklich gebraucht wird. activeTab gibt Zugriff auf den aktuellen Tab, aber erst, wenn jemand die Erweiterung aktiv aufruft. Deshalb braucht das Beispiel keinen Zugriff auf alle Websites.

Schritt 2: das Popup

Das Popup ist eine normale HTML-Seite. Wichtig: Unter Manifest V3 sind Inline-Skripte in Erweiterungsseiten verboten. JavaScript muss als eigene Datei eingebunden werden.

<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <h1>Lesezeit</h1>
    <p id="ergebnis">Wird berechnet …</p>
    <script src="popup.js"></script>
  </body>
</html>

Die Logik in popup.js fragt den aktiven Tab ab und führt dort eine kleine Funktion aus, die die Wörter zählt:

const WOERTER_PRO_MINUTE = 200; // grobe Annahme, frei anpassbar
const ausgabe = document.getElementById("ergebnis");

async function berechnen() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const [{ result: woerter }] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => document.body.innerText.split(/\s+/).filter(Boolean).length,
  });
  const minuten = Math.max(1, Math.round(woerter / WOERTER_PRO_MINUTE));
  ausgabe.textContent = `${woerter} Wörter, etwa ${minuten} Min. Lesezeit`;
}

berechnen().catch(() => {
  ausgabe.textContent = "Auf dieser Seite nicht verfügbar.";
});

chrome.scripting.executeScript benötigt die Berechtigung scripting. Die übergebene Funktion läuft im Kontext der Webseite, nicht im Popup. Sie kann deshalb keine Variablen aus popup.js sehen, nur ihr Rückgabewert kommt zurück. Der catch-Zweig fängt Seiten ab, auf denen Chrome keine Skripte erlaubt, etwa chrome://-Seiten.

Für popup.css genügt eine Breite, sonst wird das Popup sehr schmal:

body { width: 260px; font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }

Schritt 3: der Service Worker

Der Service Worker in background.js läuft ohne Fenster und reagiert auf Ereignisse. Hier legt er bei der Installation den Kontextmenü-Eintrag an und verarbeitet Klicks darauf.

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "woerter-zaehlen",
    title: "Wörter in Auswahl zählen",
    contexts: ["selection"],
  });
});

chrome.contextMenus.onClicked.addListener(async (info) => {
  if (info.menuItemId !== "woerter-zaehlen") return;
  const anzahl = info.selectionText.split(/\s+/).filter(Boolean).length;
  await chrome.storage.local.set({ letzteAuswahl: anzahl });
  await chrome.action.setBadgeText({ text: String(anzahl) });
});

Zwei Regeln sollten Sie von Anfang an einhalten:

  • Listener auf oberster Ebene registrieren. Chrome beendet den Service Worker nach kurzer Inaktivität und startet ihn beim nächsten Ereignis neu. Nur Listener, die beim Start synchron registriert werden, empfangen dieses Ereignis zuverlässig.
  • Keinen Zustand in Variablen halten. Globale Variablen gehen beim Beenden verloren. Was bleiben soll, gehört in chrome.storage.

Wie Service Worker, Popup und Content Scripts zusammenspielen und Nachrichten austauschen, vertieft der Artikel Aufbau einer Browser-Erweiterung.

Schritt 4: optional ein Content Script

Das Beispiel injiziert Code nur bei Bedarf. Soll eine Erweiterung dagegen auf bestimmten Seiten immer aktiv sein, tragen Sie ein Content Script fest im Manifest ein:

"content_scripts": [
  {
    "matches": ["https://example.com/*"],
    "js": ["content.js"],
    "run_at": "document_idle"
  }
]

Das Muster in matches bestimmt, auf welchen Adressen das Skript läuft. Jedes Muster erzeugt bei der Installation einen Berechtigungshinweis. Breite Muster wie <all_urls> führen zu der Warnung, die Erweiterung könne alle Daten auf allen Websites lesen und ändern. Das schreckt Nutzer ab und verlängert in der Regel die Prüfung im Store. Bevorzugen Sie deshalb, wie im Beispiel, activeTab mit scripting.

Optional: eine Optionsseite für Einstellungen

Die Lesegeschwindigkeit von 200 Wörtern pro Minute ist nur eine Annahme. Wer schneller oder langsamer liest, soll den Wert selbst festlegen können. Dafür gibt es die Optionsseite, die Sie im Manifest mit "options_page": "options.html" registrieren. Chrome verlinkt sie dann in den Details der Erweiterung und im Kontextmenü des Symbols.

Die Optionsseite ist wie das Popup eine normale HTML-Seite mit eigener Skriptdatei. Sie schreibt den gewählten Wert in den Speicher:

// options.js
const feld = document.getElementById("tempo");
const { tempo = 200 } = await chrome.storage.sync.get("tempo");
feld.value = tempo;
feld.addEventListener("change", () => {
  chrome.storage.sync.set({ tempo: Number(feld.value) });
});

Binden Sie das Skript mit <script type="module" src="options.js"></script> ein, damit das await auf oberster Ebene funktioniert. chrome.storage.sync gleicht die Einstellung über das Google-Konto zwischen Geräten ab, sofern die Synchronisierung aktiv ist. Für größere Datenmengen ist chrome.storage.local die bessere Wahl, weil der synchronisierte Speicher eng begrenzt ist. Im Popup lesen Sie den Wert anschließend mit chrome.storage.sync.get("tempo") statt mit der festen Konstante. So bleibt die Logik an einer Stelle, und das Popup reagiert sofort auf geänderte Einstellungen, weil es bei jedem Öffnen neu geladen wird.

Schritt 5: laden, testen, Fehler finden

  1. Öffnen Sie chrome://extensions und aktivieren Sie oben rechts den Entwicklermodus.
  2. Klicken Sie auf „Entpackte Erweiterung laden“ und wählen Sie den Ordner lesezeit.
  3. Heften Sie das Symbol über das Puzzle-Symbol in der Symbolleiste an und öffnen Sie eine beliebige Webseite.
  4. Klicken Sie auf das Symbol. Das Popup zeigt Wortzahl und Lesezeit.
  5. Markieren Sie Text, öffnen Sie das Kontextmenü und wählen Sie „Wörter in Auswahl zählen“.

Nach jeder Code-Änderung klicken Sie auf der Erweiterungskarte auf das Neuladen-Symbol. Geöffnete Tabs müssen Sie bei Content Scripts zusätzlich neu laden. Fehler finden Sie an drei Stellen:

Bestandteil Wo Sie Fehler sehen
Popup Rechtsklick ins geöffnete Popup, dann „Untersuchen“
Service Worker Link „Service Worker“ auf der Erweiterungskarte
Content Script Entwicklerwerkzeuge der Webseite, Konsole
Manifest Fehlermeldung direkt beim Laden auf der Erweiterungskarte

Schritt 6: Icons, Paket und Store

Für den Store brauchen Sie mindestens ein Symbol in 128 × 128 Pixeln, besser alle drei Größen aus dem Manifest. Hochgeladen wird ein ZIP-Archiv des Ordners, wobei die manifest.json auf oberster Ebene liegen muss, nicht in einem Unterordner.

Vor der Einreichung lohnt ein letzter Blick auf die Berechtigungen: Wird jede davon tatsächlich verwendet? Unbenutzte Berechtigungen gehören zu den häufigsten Ablehnungsgründen. Welche Kosten, Prüfzeiten und Stolpersteine im Chrome Web Store auf Sie zukommen, beschreibt der Beitrag Chrome Web Store: Gebühr, Prüfdauer und Ablehnungsgründe.

Häufige Fehler beim ersten Projekt

Symptom Ursache Lösung
Popup bleibt leer, Konsole meldet CSP-Fehler Inline-Skript im HTML Skript in eigene Datei auslagern
Kontextmenü fehlt Listener für onInstalled nicht ausgelöst Erweiterung entfernen und neu laden
Cannot access a chrome:// URL interne Seite Fehler abfangen, Hinweis anzeigen
Daten nach einiger Zeit weg Service Worker wurde beendet chrome.storage statt Variablen
chrome.scripting ist undefined Berechtigung fehlt scripting in permissions ergänzen

Berechtigungen von Anfang an sparsam halten

Jede Berechtigung im Manifest prüft der Store, und viele davon erscheinen bei der Installation als Warnung. Gewöhnen Sie sich deshalb schon im ersten Projekt an, nur das anzufordern, was der Code tatsächlich nutzt:

  • activeTab statt Zugriff auf alle Websites, wenn die Erweiterung erst nach einem Klick auf die aktuelle Seite zugreift.
  • host_permissions eng fassen, also auf die benötigten Domains statt auf <all_urls>.
  • Selten genutzte Funktionen optional machen: Unter optional_permissions eingetragene Rechte fordern Sie erst bei Bedarf mit chrome.permissions.request() an.
  • Nichts auf Vorrat eintragen: Kommt bei einem Update eine Berechtigung mit neuer Warnung hinzu, deaktiviert Chrome die Erweiterung, bis Nutzer zustimmen.

Wie Nutzer solche Hinweise lesen, erklärt der Beitrag Welche Berechtigungen von Browser-Erweiterungen sind gefährlich?.

Wie es weitergeht

Mit diesem Gerüst haben Sie alle wichtigen Bausteine einmal benutzt. Sinnvolle nächste Schritte sind eine Optionsseite für die Lesegeschwindigkeit, Mehrsprachigkeit über den Ordner _locales und die Methode chrome.i18n.getMessage sowie automatisierte Tests. Da Edge, Brave und Opera auf Chromium basieren, läuft dieselbe Erweiterung dort in der Regel ohne Änderung. Für Firefox und Safari sind kleinere Anpassungen nötig, vor allem beim Hintergrundteil im Manifest.

faq

Fragen und Antworten

Welche Programmiersprache brauche ich für eine Chrome-Erweiterung?
JavaScript, HTML und CSS. Viele Projekte nutzen TypeScript, das vor dem Laden in JavaScript übersetzt wird. Andere Sprachen kommen nur über WebAssembly in Frage, was für den Einstieg unnötig ist.
Kostet es Geld, eine Chrome-Erweiterung zu entwickeln?
Entwickeln und lokal testen ist kostenlos. Erst für die Veröffentlichung im Chrome Web Store fällt eine einmalige Registrierungsgebühr für das Entwicklerkonto an.
Warum funktioniert meine Erweiterung auf chrome://-Seiten nicht?
Chrome erlaubt Erweiterungen keinen Skriptzugriff auf interne Seiten wie chrome://settings oder auf den Chrome Web Store selbst. Fangen Sie diesen Fall im Code ab und zeigen Sie einen Hinweis an, statt einen Fehler zu werfen.
Kann ich die Erweiterung ohne Store an Kollegen weitergeben?
Zum Testen können andere den entpackten Ordner im Entwicklermodus laden. Für den dauerhaften Einsatz in einer Organisation ist die Verteilung über den Store, auch als nicht gelistete oder private Erweiterung, oder über Unternehmensrichtlinien der saubere Weg.