AtlasVoice 2.4 beta is here: natural voices, now free. Try the beta → (opens in a new tab)

AtlasVoice (opens in a new tab)

How to Add Text to Speech to Any Website (JavaScript, API, No-Code)

By · · 7 min read

Listen to this article9:03 · natural voice by AtlasVoice
How to Add Text to Speech to Any Website (JavaScript, API, No-Code) — AtlasVoice blog

There are three ways to add text to speech to a website. You can use the browser's built-in Web Speech API with a few lines of JavaScript, generate an MP3 with a text to speech API and play it in a normal audio player, or install a ready-made plugin or embed if your site runs on a platform like WordPress. The JavaScript route is free and quick. The MP3 route sounds the same for every visitor. The no-code route needs no programming at all.

This guide walks through all three, with working code, the honest limits of each, and the accessibility details that make a read-aloud button genuinely useful.

Option 1: Text to speech in HTML and JavaScript with the Web Speech API

Every major browser ships a speech engine you can control from JavaScript through window.speechSynthesis (opens in a new tab). MDN lists it as "Widely available" across browsers since September 2018. You create a SpeechSynthesisUtterance with the text to read, optionally pick a voice and language, and pass it to speechSynthesis.speak(). The controller also has pause(), resume() and cancel().

Three details trip up most first attempts:

  • Voices load asynchronously. In some browsers, such as Chrome, getVoices() returns an empty list until the voiceschanged event fires, so MDN's example (opens in a new tab) reads the list once and again on that event.
  • Speech has to start from a click. Since Chrome 71, speechSynthesis.speak() throws an error if the page has not received a user activation (opens in a new tab). Use a real button, and never try to start speech on page load.
  • Pick the voice by language. Voice names differ on every device, so match on voice.lang (a BCP 47 tag such as en-US) instead of hard-coding a name.

A small, accessible read aloud button for a website

This example adds a "Listen to this article" button that changes to Pause and Resume, plus a Stop button. It only appears in browsers that support speech, and it reads the voice language from the page's lang attribute.

<!-- Declare your page language: <html lang="en-US"> -->

<article id="article-body">
  <h1>Your article title</h1>
  <p>Your article text...</p>
</article>

<div id="tts-controls" hidden>
  <button type="button" id="tts-play">Listen to this article</button>
  <button type="button" id="tts-stop" hidden>Stop</button>
</div>

<script>
(function () {
  if (!('speechSynthesis' in window)) return; // controls stay hidden

  const synth = window.speechSynthesis;
  const controls = document.getElementById('tts-controls');
  const playBtn = document.getElementById('tts-play');
  const stopBtn = document.getElementById('tts-stop');
  const article = document.getElementById('article-body');
  controls.hidden = false;

  // The first call can return [] because voices load asynchronously.
  let voices = synth.getVoices();
  synth.addEventListener('voiceschanged', () => {
    voices = synth.getVoices();
  });

  function pickVoice(lang) {
    const want = lang.toLowerCase();
    const base = want.split('-')[0];
    const norm = (v) => v.lang.replace('_', '-').toLowerCase();
    return voices.find((v) => norm(v) === want)
      || voices.find((v) => norm(v).split('-')[0] === base)
      || null;
  }

  // Track our own state: speechSynthesis.paused can stay true after cancel().
  let state = 'idle';
  let current = null;

  function setState(next) {
    state = next;
    playBtn.textContent =
      next === 'playing' ? 'Pause' :
      next === 'paused' ? 'Resume' : 'Listen to this article';
    stopBtn.hidden = next === 'idle';
  }

  playBtn.addEventListener('click', () => {
    if (state === 'playing') {
      synth.pause();
      setState('paused');
      return;
    }
    if (state === 'paused') {
      synth.resume();
      setState('playing');
      return;
    }

    synth.cancel(); // clear anything still queued
    const lang = document.documentElement.lang || 'en-US';
    const utterance = new SpeechSynthesisUtterance(article.innerText);
    utterance.lang = lang;
    const voice = pickVoice(lang);
    if (voice) utterance.voice = voice;

    const done = () => {
      if (current === utterance) setState('idle');
    };
    utterance.addEventListener('end', done);
    utterance.addEventListener('error', done);

    current = utterance;
    synth.speak(utterance);
    if (synth.paused) synth.resume();
    setState('playing');
  });

  stopBtn.addEventListener('click', () => {
    current = null;
    synth.cancel();
    setState('idle');
  });
})();
</script>

A few notes on the code. cancel() empties the whole queue, so it is safe to call before starting. The script keeps its own state instead of trusting speechSynthesis.paused, because in our Chrome testing that flag stayed true after pausing and then stopping, which would leave a naive button stuck. You can set utterance.rate between 0.1 and 10 (1 is normal), though MDN notes that some engines narrow that range. For long articles, many production players speak one paragraph at a time, which also makes it easy to highlight or skip sections.

The honest limits of browser text to speech

  • Voices differ per device and browser. getVoices() returns the voices "available on the current device", so the same page can sound quite different on Windows, macOS, Android and iOS, and a language may have no voice at all.
  • Some voices are not local. A voice with localService set to false comes from a remote speech service, which matters if your text is private.
  • No MP3. The API speaks; it has no method for saving the audio, so visitors cannot download it or listen offline.
  • Mobile is less predictable. Phones usually offer fewer voices, and background playback and pausing behave differently between mobile browsers, so test on real devices.

If those limits are fine for your site, you are done: the whole feature costs nothing and needs no account or server.

Option 2: Pre-generated MP3 with a text to speech API

If you want the same natural voice on every device, or a file people can download, generate the audio once on your server and play it with a plain <audio> element. The flow looks like this:

  1. When you publish or update a page, your server sends its text to a text to speech API.
  2. The API returns an MP3, and you store it with your content (your media folder, a CDN or object storage).
  3. The page shows a standard audio player that points at that file.

Never put an API key in browser JavaScript. Anything the browser downloads, visitors can read, so a key in front-end code can be copied and used on your account. Make the call from your backend or a build script, and keep the key in an environment variable.

Here is the first request from the AtlasVoice API (opens in a new tab) (our own service), which has a free plan of 100,000 characters a month:

curl -X POST "https://api.atlasvoice.cloud/v1/synthesize?raw=1" \
  -H "Authorization: Bearer $ATLASVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello from AtlasVoice.", "lang": "en-US"}' \
  -o hello.mp3

?raw=1 returns the MP3 itself. Each request takes up to 5,000 characters, so longer articles are sent in parts and joined. Generated files are deleted from AtlasVoice servers within minutes, which is why you save the MP3 and serve it from your own site. The API page (opens in a new tab) shows the same request in Node.js, PHP and Python. Other providers work the same way; our comparison of free text to speech APIs (opens in a new tab) looks at the options.

Then play the file with native controls:

<p id="audio-label">Listen to this article</p>
<audio controls preload="none" aria-labelledby="audio-label"
       src="/audio/my-article.mp3">
  <a href="/audio/my-article.mp3">Download the audio (MP3)</a>
</audio>

controls gives visitors the browser's own keyboard-accessible player, and preload="none" avoids downloading audio for people who never press play. The link inside is fallback content for browsers that do not support the audio element.

Option 3: No-code text to speech for web pages

WordPress

On WordPress, a plugin handles everything for you. Our free plugin, AtlasVoice (Text To Speech TTS Accessibility) (opens in a new tab), adds a Listen button to posts, pages and products using browser voices, with no account or API key, and you can place it anywhere with the [atlasvoice] shortcode. The step-by-step setup is in our guide to adding text to speech to WordPress (opens in a new tab). The next release, currently in public beta, adds a free natural voice that is saved as an MP3 on your site.

Shopify, Wix, Webflow and Squarespace

AtlasVoice does not have apps for these platforms yet, but the MP3 pattern from Option 2 works on all of them:

  1. Generate the MP3 with any text to speech API or tool.
  2. Upload it to the platform's file or media library (in Shopify, that is Content > Files).
  3. Add a player. Squarespace has an Audio block and Wix has an Audio Player element that take an uploaded MP3. In Webflow, paste the <audio> snippet above into a Code Embed element. In Shopify, use a Custom Liquid section if your theme offers one.

It is manual work per page, so it suits a handful of key pages better than a large blog.

Choosing how to add text to speech to your website

ApproachCostVoiceDownloadable fileSetupBest for
Web Speech API (JavaScript)FreeVaries by device and browserNoA few lines of codeQuick read aloud buttons, prototypes
TTS API + MP3Per character, often with a free tierSame natural voice everywhereYesServer code and file storageCustom sites, apps, consistent audio
WordPress pluginFree to startBrowser voices, or MP3 voicesWith MP3 voicesInstall and configureWordPress blogs and stores
Upload MP3 on a site builderDepends on how you generate audioWhatever you generatedYesManual per pageA few key pages on Shopify, Wix, Webflow or Squarespace

Accessibility tips for your listen button

  • Use a real <button> or native <audio controls>. Both work with the keyboard (Tab, Enter, Space) and screen readers without extra code. A clickable <div> does not.
  • Give it a clear label. "Listen to this article" says what happens; an icon alone does not.
  • Never autoplay. Unexpected sound clashes with screen readers and startles everyone else. Let people press play.
  • Keep controls visible. Pause and Stop should always be one press away, with a visible focus outline.
  • Set the page language. The lang attribute helps both your player and screen readers choose the right voice.

An audio version is a helpful extra, not an accessibility fix on its own. Our article on audio articles and the European Accessibility Act (opens in a new tab) explains who it helps and where it fits.

Next step

Try the JavaScript example on one page today; it costs nothing. If you need the same voice on every device, get a free AtlasVoice API key (opens in a new tab) and generate your first MP3, or install the free plugin if your site runs on WordPress.

accessibilityjavascripttext to speechweb speech api