Why a Text‑to‑Speech Chrome Extension?
If you spend any time on the web, you’ve probably wished you could listen to articles, docs, or chat messages instead of reading them. A lightweight Chrome extension that turns selected text into natural‑sounding speech can boost productivity, aid accessibility, and just be plain fun.
In this guide we’ll build a Text‑to‑Speech (TTS) Chrome extension that leverages the ElevenLabs voice AI platform. ElevenLabs offers high‑quality, neural TTS with voice cloning capabilities, and their API is straightforward to call from JavaScript. By the end of the tutorial you’ll have a working extension you can load locally, customize with your favorite voice, and ship to the Chrome Web Store.
👉 Quick link: Want to try ElevenLabs now? Grab a free trial here: https://try.elevenlabs.io/kr07zfuqn1bp
What You’ll Need
| Item | Reason |
|---|---|
| Node.js (>=14) | To run the build script and install dependencies. |
| Chrome browser | To load and test the extension. |
| ElevenLabs API key | Required to call the TTS endpoint. Sign up at the link above. |
| Basic HTML/CSS/JS | The extension’s UI is built with vanilla web tech. |
Tip: Keep your API key secret! We’ll store it in the extension’s
background.jsusing Chrome’sstorage.syncAPI so it never ends up in the public repo.
1. Project Structure
Create a new folder called tts-extension and add the following files:
tts-extension/
│�� manifest.json
│─ background.js
│─ popup/
│ ├─ popup.html
│ ├─ popup.css
│ └─ popup.js
└─ icons/
└─ icon48.png
The manifest.json tells Chrome what the extension can do, while the popup folder holds the UI that appears when you click the extension icon.
2. Manifest – The Blueprint
{
"manifest_version": 3,
"name": "Voice Reader",
"description": "Select text and have ElevenLabs read it aloud.",
"version": "1.0.0",
"permissions": ["storage", "activeTab", "scripting"],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"48": "icons/icon48.png"
}
},
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://api.elevenlabs.io/*"]
}
Key points
-
manifest_version: 3is the current standard. -
scriptingpermission lets us inject a content script on the active tab to grab selected text. -
host_permissionswhitelist the ElevenLabs API domain.
3. Popup UI – Simple but Effective
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Voice Reader</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<h2>Read Selected Text</h2>
<select id="voiceSelect"></select>
<button id="speakBtn">Speak</button>
<hr>
<label for="apiKey">ElevenLabs API Key</label>
<input type="password" id="apiKey" placeholder="Enter API key">
<button id="saveKeyBtn">Save</button>
<script src="popup.js"></script>
</body>
</html>
popup.css (keep it minimal)
body { font-family: sans-serif; width: 250px; padding: 10px; }
h2 { font-size: 1.1rem; margin-bottom: 0.5rem; }
button, select, input { width: 100%; margin-top: 5px; }
The UI lets the user (a) pick a voice, (b) trigger speech, and (c) store their ElevenLabs API key securely.
4. Background Script – The Engine
background.js
// Listen for messages from popup.js
chrome.runtime.onMessage.addListener(async (msg, sender, respond) => {
if (msg.action === 'speak') {
const { text, voiceId, apiKey } = msg.payload;
try {
const audioUrl = await generateAudio(text, voiceId, apiKey);
// Play audio in a new tab (or you could use Audio() directly)
chrome.tabs.create({ url: audioUrl });
respond({ success: true });
} catch (e) {
console.error(e);
respond({ success: false, error: e.message });
}
return true; // keep the channel open for async response
}
});
/**
* Calls ElevenLabs TTS endpoint and returns a data: URL with the MP3.
*/
async function generateAudio(text, voiceId, apiKey) {
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xi-api-key': apiKey
},
body: JSON.stringify({
text,
model_id: 'eleven_monolingual_v1',
voice_settings: { stability: 0.75, similarity_boost: 0.85 }
})
}
);
if (!response.ok) {
const err = await response.text();
throw new Error(`ElevenLabs error: ${err}`);
}
const blob = await response.blob();
return URL.createObjectURL(blob); // returns a temporary URL to the MP3
}
Why we use a background service worker
Chrome MV3 forces us to use a service worker instead of a persistent background page. The worker stays alive only while we’re processing a request, keeping the extension lightweight.
5. Popup Logic – Glue Between UI and Background
popup.js
const voiceSelect = document.getElementById('voiceSelect');
const speakBtn = document.getElementById('speakBtn');
const apiKeyInput = document.getElementById('apiKey');
const saveKeyBtn = document.getElementById('saveKeyBtn');
const ELEVENLABS_API = 'https://api.elevenlabs.io/v1/voices';
// Load saved API key
chrome.storage.sync.get(['apiKey'], (data) => {
if (data.apiKey) apiKeyInput.value = data.apiKey;
});
// Save API key
saveKeyBtn.onclick = () => {
const key = apiKeyInput.value.trim();
if (key) {
chrome.storage.sync.set({ apiKey: key }, () => alert('API key saved!'));
}
};
// Populate voice dropdown
async function loadVoices() {
const key = apiKeyInput.value.trim();
if (!key) return;
const res = await fetch(ELEVENLABS_API, {
headers: { 'xi-api-key': key }
});
const { voices } = await res.json();
voiceSelect.innerHTML = voices
.map(v => `<option value="${v.voice_id}">${v.name}</option>`)
.join('');
}
apiKeyInput.addEventListener('blur', loadVoices);
// When user clicks "Speak"
speakBtn.onclick = async () => {
const [{ result: { selectionText } }] = await chrome.scripting.executeScript({
target: { tabId: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].id },
func: () => ({ selectionText: window.getSelection().toString() })
});
if (!selectionText) {
alert('Please select some text on the page first.');
return;
}
const voiceId = voiceSelect.value;
const apiKey = apiKeyInput.value.trim();
chrome.runtime.sendMessage(
{
action: 'speak',
payload: { text: selectionText, voiceId, apiKey }
},
(response) => {
if (!response.success) alert(`Error: ${response.error}`);
}
);
};
The script does three things:
-
Persist the API key using Chrome’s
storage.sync. - Fetch available voices from ElevenLabs and fill the dropdown.
- Grab the selected text on the active tab, then ask the background worker to synthesize and play it.
6. Testing Locally
- Open Chrome →
chrome://extensions/. - Enable Developer mode (top right).
- Click Load unpacked and select the
tts-extensionfolder. - Pin the extension icon, click it, paste your ElevenLabs API key, and hit Save.
- Navigate to any article, highlight a sentence, and press Speak.
If everything is wired correctly you’ll see a new tab open with the generated audio playing. You can also replace the chrome.tabs.create call with an Audio object if you prefer in‑page playback.
7. Going Further – Voice Cloning & Customization
ElevenLabs shines when you upload a short sample (≈30 seconds) and let the service create a cloned voice. The API for cloning is identical; you just use the voice_id that the cloning endpoint returns. Once you have a custom voice_id, drop it into the dropdown or hard‑code it for a personal assistant feel.
You can also experiment with the stability and similarity_boost parameters to make the speech sound more expressive or more like the source voice. Play with values between 0 and 1 until you find the sweet spot for your use case.
8. Packaging for the Chrome Web Store
When you’re ready to share your extension:
- Run
npm run build(if you set up a bundler) or simply zip the folder. - Make sure manifest.json version is bumped.
- Upload the zip to the Chrome Web Store Developer Dashboard.
- Fill out the required store listing fields, add screenshots, and submit for review.
Remember to remove any hard‑coded API keys before publishing! Use the storage flow we built above, and let users provide their own keys.
9. Common Pitfalls & Debugging
| Symptom | Likely Cause | Fix |
|---|---|---|
| No audio plays |
chrome.tabs.create blocked by popup blocker |
Use Audio object or request the tabs permission |
fetch fails with 401 |
Wrong or missing API key | Verify the key is saved correctly; check for extra spaces |
| Voices list stays empty | API key not set before fetching | Click out of the API key field (blur) or add a manual “Load Voices” button |
| Extension disappears after idle | Service worker stopped | This is normal; the worker restarts when a message arrives |
Open Chrome’s Extensions → Details → Inspect views to view console logs from the background worker and popup.
10. Wrap‑Up
You now have a functional Text‑to‑Speech Chrome extension powered by ElevenLabs. The core ideas—injecting a content script, calling a cloud TTS API, and handling audio playback—are reusable for many other projects, such as:
- A “read‑aloud” button for PDFs or e‑books.
- An accessibility overlay for form fields.
- A voice‑enabled chatbot that speaks its replies.
The real magic lies in the quality of the voice model, and ElevenLabs provides some of the most natural‑sounding options available today.
Ready to give your extension a premium voice? Try ElevenLabs with the affiliate link and unlock the full suite of voice cloning tools: https://try.elevenlabs.io/kr07zfuqn1bp
Happy coding, and enjoy the sound of your own words!
Top comments (0)