DEV Community

VoiceDeveloper
VoiceDeveloper

Posted on

Build a Text-to-Speech Chrome Extension

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.js using Chrome’s storage.sync API 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
Enter fullscreen mode Exit fullscreen mode

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/*"]
}
Enter fullscreen mode Exit fullscreen mode

Key points

  • manifest_version: 3 is the current standard.
  • scripting permission lets us inject a content script on the active tab to grab selected text.
  • host_permissions whitelist 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>
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}`);
    }
  );
};
Enter fullscreen mode Exit fullscreen mode

The script does three things:

  1. Persist the API key using Chrome’s storage.sync.
  2. Fetch available voices from ElevenLabs and fill the dropdown.
  3. Grab the selected text on the active tab, then ask the background worker to synthesize and play it.

6. Testing Locally

  1. Open Chrome → chrome://extensions/.
  2. Enable Developer mode (top right).
  3. Click Load unpacked and select the tts-extension folder.
  4. Pin the extension icon, click it, paste your ElevenLabs API key, and hit Save.
  5. 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:

  1. Run npm run build (if you set up a bundler) or simply zip the folder.
  2. Make sure manifest.json version is bumped.
  3. Upload the zip to the Chrome Web Store Developer Dashboard.
  4. 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)