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
- Content script – grabs the user’s selected text and sends it to the background script.
-
Background script – calls the ElevenLabs TTS endpoint, receives an audio buffer, and creates a temporary
blob:URL. - 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
- Sign up at the official ElevenLabs site using this link: https://try.elevenlabs.io/kr07zfuqn1bp.
- Once logged in, navigate to API → Keys and generate a new key.
- 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
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"]
}
]
}
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;
}
});
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;
}
});
Important: Replace
EXAMPLE_VOICE_IDwith 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>
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();
});
});
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)
You would then change background.js to call http://localhost:5000/tts instead of the ElevenLabs endpoint directly.
Testing the Extension
- Open
chrome://extensions/. - Enable Developer mode (top‑right).
- Click Load unpacked and select the
tts-extensionfolder. - 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.localto 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.jsonfor 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!
Top comments (0)