Build a Text-to-Speech Chrome Extension

# webdev# tutorial# javascript# ai
Build a Text-to-Speech Chrome ExtensionVoiceDeveloper

Why a Text‑to‑Speech Chrome Extension? If you spend a lot of time reading articles,...

Why a Text‑to‑Speech Chrome Extension?

If you spend a lot of time reading articles, documentation, or even chat threads, you’ve probably wished for a quick way to listen instead of scrolling. A Chrome extension that turns any selected text into natural‑sounding speech can boost productivity, reduce eye strain, and make the web more accessible. In this post we’ll build a minimalist TTS extension that uses ElevenLabs – a state‑of‑the‑art voice AI platform – to generate high‑quality audio on the fly.

Note: The API key you obtain from ElevenLabs is free for a generous amount of usage, and the affiliate link below helps us keep the guide up‑to‑date. No extra cost for you.

Overview of the Architecture

  1. Content script – grabs the user’s selected text and sends it to the background script.
  2. Background script – calls the ElevenLabs TTS endpoint, receives an audio buffer, and creates a temporary blob: URL.
  3. Popup UI – a small button that toggles playback and shows a simple loading spinner.

All the heavy lifting is done in JavaScript; we only need a tiny Python helper (optional) to fetch the API key securely if you prefer not to embed it directly.

Getting an ElevenLabs API Key

  1. Sign up at the official ElevenLabs site using this link: https://try.elevenlabs.io/kr07zfuqn1bp.
  2. Once logged in, navigate to API → Keys and generate a new key.
  3. Copy the key – you’ll need it for the extension’s manifest.json.

Tip: Keep the key out of version control. In production you’d store it on a server and proxy the request, but for a local demo embedding it is fine.

Project Structure

tts-extension/
│
├─ manifest.json
├─ background.js
├─ content.js
├─ popup.html
├─ popup.js
└─ icons/
   └─ icon48.png
Enter fullscreen mode Exit fullscreen mode

Step 1 – manifest.json

We’ll target Manifest V3, the current standard for Chrome extensions.

{
  "manifest_version": 3,
  "name": "Quick TTS",
  "description": "Select text and listen instantly using ElevenLabs AI voices.",
  "version": "1.0",
  "permissions": ["activeTab", "scripting", "storage"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "48": "icons/icon48.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 2 – Content Script (content.js)

The content script watches for a custom event that the popup will dispatch. When triggered, it reads the current selection and forwards it to the background script.

// content.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "speakSelection") {
    const selection = window.getSelection().toString().trim();
    if (!selection) {
      sendResponse({ error: "No text selected." });
      return;
    }
    // Forward the text to the background worker
    chrome.runtime.sendMessage(
      { action: "ttsRequest", text: selection },
      (response) => sendResponse(response)
    );
    // Keep the message channel alive for async response
    return true;
  }
});
Enter fullscreen mode Exit fullscreen mode

Step 3 – Background Service Worker (background.js)

Here we call the ElevenLabs API. The endpoint expects a POST with JSON payload and returns an audio/mpeg stream.

// background.js
const ELEVENLABS_API_KEY = "YOUR_ELEVENLABS_API_KEY"; // <-- replace

async function fetchAudio(text) {
  const url = "https://api.elevenlabs.io/v1/text-to-speech/EXAMPLE_VOICE_ID";
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "xi-api-key": ELEVENLABS_API_KEY
    },
    body: JSON.stringify({
      text,
      model_id: "eleven_monolingual_v1",
      voice_settings: { stability: 0.5, similarity_boost: 0.75 }
    })
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`ElevenLabs error: ${err}`);
  }
  return await response.arrayBuffer();
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.action === "ttsRequest") {
    fetchAudio(msg.text)
      .then((buffer) => {
        const blob = new Blob([buffer], { type: "audio/mpeg" });
        const url = URL.createObjectURL(blob);
        sendResponse({ audioUrl: url });
      })
      .catch((err) => sendResponse({ error: err.message }));
    // Asynchronous response
    return true;
  }
});
Enter fullscreen mode Exit fullscreen mode

Important: Replace EXAMPLE_VOICE_ID with the voice you like from ElevenLabs. You can list your available voices via their API or UI.

Step 4 – Popup UI (popup.html)

A tiny UI with a button and a spinner.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Quick TTS</title>
  <style>
    body { font-family: sans-serif; width: 200px; padding: 10px; }
    #playBtn { width: 100%; padding: 8px; }
    #spinner { display: none; margin-top: 8px; }
  </style>
</head>
<body>
  <button id="playBtn">🔊 Speak Selection</button>
  <div id="spinner">Loading…</div>
  <script src="popup.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 5 – Popup Logic (popup.js)

The script triggers the content script, receives the audio URL, and plays it.

// popup.js
const btn = document.getElementById("playBtn");
const spinner = document.getElementById("spinner");
let audio = null;

btn.addEventListener("click", async () => {
  btn.disabled = true;
  spinner.style.display = "block";

  // Ask the active tab's content script to grab the selection
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  chrome.tabs.sendMessage(tab.id, { action: "speakSelection" }, (response) => {
    spinner.style.display = "none";
    btn.disabled = false;

    if (response?.error) {
      alert(response.error);
      return;
    }

    // Play the returned audio URL
    if (audio) audio.pause();
    audio = new Audio(response.audioUrl);
    audio.play();
  });
});
Enter fullscreen mode Exit fullscreen mode

Optional: Secure the API Key with a Python Proxy

Embedding the API key in the extension is fine for personal use, but you might want a server‑side proxy for production. Below is a minimal Flask endpoint that forwards the request to ElevenLabs while keeping the key hidden.

# server.py
from flask import Flask, request, jsonify, send_file
import requests
import io
import os

app = Flask(__name__)
ELEVEN_API_KEY = os.getenv("ELEVEN_API_KEY")  # set in your environment

@app.route("/tts", methods=["POST"])
def tts():
    data = request.json
    voice_id = data.get("voice_id", "EXAMPLE_VOICE_ID")
    url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
    resp = requests.post(
        url,
        json=data,
        headers={"xi-api-key": ELEVEN_API_KEY, "Content-Type": "application/json"},
        stream=True,
    )
    resp.raise_for_status()
    return send_file(
        io.BytesIO(resp.content),
        mimetype="audio/mpeg",
        as_attachment=False,
        download_name="speech.mp3",
    )

if __name__ == "__main__":
    app.run(port=5000)
Enter fullscreen mode Exit fullscreen mode

You would then change background.js to call http://localhost:5000/tts instead of the ElevenLabs endpoint directly.

Testing the Extension

  1. Open chrome://extensions/.
  2. Enable Developer mode (top‑right).
  3. Click Load unpacked and select the tts-extension folder.
  4. Pin the extension, navigate to any webpage, select a paragraph, and hit Speak Selection in the popup.

If everything works, you should hear the selected text spoken in the voice you chose from ElevenLabs.

Tweaking Voice Settings

ElevenLabs exposes several parameters:

Parameter Range Effect
stability 0‑1 Controls how steady the voice sounds. Lower values add more “human‑like” variance.
similarity_boost 0‑1 Makes the voice more like the reference speaker. Higher values improve likeness but can reduce naturalness.

Feel free to experiment by updating the voice_settings object in background.js. The changes are reflected instantly—no need to rebuild the extension.

Where to Go Next?

  • Cache audio: Store generated blobs in chrome.storage.local to avoid re‑requesting the same sentence.
  • Multi‑language support: ElevenLabs now offers several language models; just swap model_id.
  • Context menu: Add a right‑click option to speak without opening the popup.
  • Keyboard shortcut: Define a command in manifest.json for a one‑key trigger.

All of these enhancements are straightforward additions once you have the core pipeline in place.


Ready to give your Chrome browsing a voice?

ElevenLabs makes it easy to generate lifelike speech with just a few lines of code. Grab your API key through this link https://try.elevenlabs.io/kr07zfuqn1bp, follow the steps above, and you’ll have a functional TTS extension in under ten minutes. Happy coding, and enjoy the sound of the web!