{"slug": "build-a-text-to-speech-chrome-extension", "title": "Build a Text-to-Speech Chrome Extension", "summary": "A developer published a tutorial for building a Chrome extension, dubbed Voice Reader, that converts selected web text into speech using the ElevenLabs neural text-to-speech API. The guide walks through a Manifest V3 extension with a popup UI for voice selection and API key storage, a background service worker that calls the ElevenLabs TTS endpoint, and host permissions scoped to the API domain.", "body_md": "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.\n\nIn 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.\n\n👉 **Quick link:** Want to try ElevenLabs now? Grab a free trial here: [https://try.elevenlabs.io/kr07zfuqn1bp](https://try.elevenlabs.io/kr07zfuqn1bp)\n\n| Item | Reason | \n|---|---|\n| **Node.js (>=14)** | To run the build script and install dependencies. | \n| **Chrome browser** | To load and test the extension. | \n| **ElevenLabs API key** | Required to call the TTS endpoint. Sign up at the link above. | \n| **Basic HTML/CSS/JS** | The extension’s UI is built with vanilla web tech. | \n\n**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.\n\nCreate a new folder called `tts-extension` and add the following files:\n\n```\ntts-extension/\n│�� manifest.json\n│─ background.js\n│─ popup/\n│   ├─ popup.html\n│   ├─ popup.css\n│   └─ popup.js\n└─ icons/\n    └─ icon48.png\n```\n\nThe `manifest.json` tells Chrome what the extension can do, while the `popup` folder holds the UI that appears when you click the extension icon.\n\n```\n{\n  \"manifest_version\": 3,\n  \"name\": \"Voice Reader\",\n  \"description\": \"Select text and have ElevenLabs read it aloud.\",\n  \"version\": \"1.0.0\",\n  \"permissions\": [\"storage\", \"activeTab\", \"scripting\"],\n  \"action\": {\n    \"default_popup\": \"popup/popup.html\",\n    \"default_icon\": {\n      \"48\": \"icons/icon48.png\"\n    }\n  },\n  \"background\": {\n    \"service_worker\": \"background.js\"\n  },\n  \"host_permissions\": [\"https://api.elevenlabs.io/*\"]\n}\n```\n\n*Key points* \n\n`manifest_version: 3` is the current standard.\n`scripting` permission lets us inject a content script on the active tab to grab selected text.\n`host_permissions` whitelist the ElevenLabs API domain.\n**popup.html**\n\n```\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Voice Reader</title>\n  <link rel=\"stylesheet\" href=\"popup.css\">\n</head>\n<body>\n  <h2>Read Selected Text</h2>\n  <select id=\"voiceSelect\"></select>\n  <button id=\"speakBtn\">Speak</button>\n\n  <hr>\n  <label for=\"apiKey\">ElevenLabs API Key</label>\n  <input type=\"password\" id=\"apiKey\" placeholder=\"Enter API key\">\n  <button id=\"saveKeyBtn\">Save</button>\n\n  <script src=\"popup.js\"></script>\n</body>\n</html>\n```\n\n**popup.css** (keep it minimal)\n\n```\nbody { font-family: sans-serif; width: 250px; padding: 10px; }\nh2 { font-size: 1.1rem; margin-bottom: 0.5rem; }\nbutton, select, input { width: 100%; margin-top: 5px; }\n```\n\nThe UI lets the user (a) pick a voice, (b) trigger speech, and (c) store their ElevenLabs API key securely.\n\n**background.js**\n\n```\n// Listen for messages from popup.js\nchrome.runtime.onMessage.addListener(async (msg, sender, respond) => {\n  if (msg.action === 'speak') {\n    const { text, voiceId, apiKey } = msg.payload;\n    try {\n      const audioUrl = await generateAudio(text, voiceId, apiKey);\n      // Play audio in a new tab (or you could use Audio() directly)\n      chrome.tabs.create({ url: audioUrl });\n      respond({ success: true });\n    } catch (e) {\n      console.error(e);\n      respond({ success: false, error: e.message });\n    }\n    return true; // keep the channel open for async response\n  }\n});\n\n/**\n * Calls ElevenLabs TTS endpoint and returns a data: URL with the MP3.\n */\nasync function generateAudio(text, voiceId, apiKey) {\n  const response = await fetch(\n    `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,\n    {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'xi-api-key': apiKey\n      },\n      body: JSON.stringify({\n        text,\n        model_id: 'eleven_monolingual_v1',\n        voice_settings: { stability: 0.75, similarity_boost: 0.85 }\n      })\n    }\n  );\n\n  if (!response.ok) {\n    const err = await response.text();\n    throw new Error(`ElevenLabs error: ${err}`);\n  }\n\n  const blob = await response.blob();\n  return URL.createObjectURL(blob); // returns a temporary URL to the MP3\n}\n```\n\nChrome 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.\n\n**popup.js**\n\n``` js\nconst voiceSelect = document.getElementById('voiceSelect');\nconst speakBtn = document.getElementById('speakBtn');\nconst apiKeyInput = document.getElementById('apiKey');\nconst saveKeyBtn = document.getElementById('saveKeyBtn');\n\nconst ELEVENLABS_API = 'https://api.elevenlabs.io/v1/voices';\n\n// Load saved API key\nchrome.storage.sync.get(['apiKey'], (data) => {\n  if (data.apiKey) apiKeyInput.value = data.apiKey;\n});\n\n// Save API key\nsaveKeyBtn.onclick = () => {\n  const key = apiKeyInput.value.trim();\n  if (key) {\n    chrome.storage.sync.set({ apiKey: key }, () => alert('API key saved!'));\n  }\n};\n\n// Populate voice dropdown\nasync function loadVoices() {\n  const key = apiKeyInput.value.trim();\n  if (!key) return;\n  const res = await fetch(ELEVENLABS_API, {\n    headers: { 'xi-api-key': key }\n  });\n  const { voices } = await res.json();\n  voiceSelect.innerHTML = voices\n    .map(v => `<option value=\"${v.voice_id}\">${v.name}</option>`)\n    .join('');\n}\napiKeyInput.addEventListener('blur', loadVoices);\n\n// When user clicks \"Speak\"\nspeakBtn.onclick = async () => {\n  const [{ result: { selectionText } }] = await chrome.scripting.executeScript({\n    target: { tabId: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].id },\n    func: () => ({ selectionText: window.getSelection().toString() })\n  });\n\n  if (!selectionText) {\n    alert('Please select some text on the page first.');\n    return;\n  }\n\n  const voiceId = voiceSelect.value;\n  const apiKey = apiKeyInput.value.trim();\n\n  chrome.runtime.sendMessage(\n    {\n      action: 'speak',\n      payload: { text: selectionText, voiceId, apiKey }\n    },\n    (response) => {\n      if (!response.success) alert(`Error: ${response.error}`);\n    }\n  );\n};\n```\n\nThe script does three things:\n\n`storage.sync`.\n`chrome://extensions/`.\n`tts-extension` folder.\nIf 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.\n\nElevenLabs 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.\n\nYou 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.\n\nWhen you’re ready to share your extension:\n\n`npm run build` (if you set up a bundler) or simply zip the folder.\nRemember to **remove** any hard‑coded API keys before publishing! Use the storage flow we built above, and let users provide their own keys.\n\n| Symptom | Likely Cause | Fix | \n|---|---|---|\n| No audio plays | `chrome.tabs.create` blocked by popup blocker | Use `Audio` object or request the`tabs` permission | \n| `fetch` fails with 401 | Wrong or missing API key | Verify the key is saved correctly; check for extra spaces | \n| 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 | \n| Extension disappears after idle | Service worker stopped | This is normal; the worker restarts when a message arrives | \n\nOpen Chrome’s **Extensions → Details → Inspect views** to view console logs from the background worker and popup.\n\nYou 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:\n\nThe real magic lies in the quality of the voice model, and ElevenLabs provides some of the most natural‑sounding options available today.\n\n**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](https://try.elevenlabs.io/kr07zfuqn1bp)\n\nHappy coding, and enjoy the sound of your own words!", "url": "https://wpnews.pro/news/build-a-text-to-speech-chrome-extension", "canonical_source": "https://dev.to/voice_developer/build-a-text-to-speech-chrome-extension-5d6a", "published_at": "2026-10-18 16:13:58+00:00", "updated_at": "2026-10-18 16:17:51.162922+00:00", "lang": "en", "topics": ["ai-tools", "generative-ai", "developer-tools"], "entities": ["ElevenLabs", "Google Chrome", "Chrome Web Store", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/build-a-text-to-speech-chrome-extension", "markdown": "https://wpnews.pro/news/build-a-text-to-speech-chrome-extension.md", "text": "https://wpnews.pro/news/build-a-text-to-speech-chrome-extension.txt", "jsonld": "https://wpnews.pro/news/build-a-text-to-speech-chrome-extension.jsonld"}}