To turn text into speech in JavaScript, create a SpeechSynthesisUtterance and pass it to window.speechSynthesis.speak(). The browser handles playback. You don’t need an API key, a server, or a JavaScript package.
This works well for a prototype or a read-aloud control where device-provided voices are acceptable. If you’re building a voice agent that needs the same voice across devices or audio streamed to a phone call, use a hosted text-to-speech API instead. Browser SpeechSynthesis and Cartesia’s API are separate ways to generate speech.
How browser speech synthesis works
window.speechSynthesis is the Web Speech API’s controller for synthesized speech. An utterance holds the text and settings for one request. Calling speak() adds the utterance to a queue; it doesn’t return an audio file.1
The browser exposes the voices available on the current device through getVoices(). That list can load after your script runs, so read it immediately and refresh it when voiceschanged fires.2 Don’t hard-code a voice name from your laptop and expect it to exist on a customer’s phone.
1. Save a working text-to-speech demo
Save the following as speech.html and open it in a browser. Enter a short sentence, choose a voice if the browser lists one, and press Speak. Use nonsensitive sample text: some voices use remote speech services.3
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JavaScript text-to-speech demo</title>
</head>
<body>
<h1>Read text aloud</h1>
<p>
<label for="text">Text to speak</label><br />
<textarea id="text" rows="4" cols="30" maxlength="500">
Hello from your browser.</textarea
>
</p>
<p>
<label for="voice">Voice</label>
<select id="voice"></select>
</p>
<button id="speak" type="button">Speak</button>
<button id="stop" type="button">Stop</button>
<p id="status" role="status" aria-live="polite">Ready.</p>
<script>
const text = document.querySelector("#text");
const voiceSelect = document.querySelector("#voice");
const speakButton = document.querySelector("#speak");
const stopButton = document.querySelector("#stop");
const status = document.querySelector("#status");
let voices = [];
let currentUtterance = null;
if (
!("speechSynthesis" in window) ||
!("SpeechSynthesisUtterance" in window)
) {
status.textContent = "This browser does not support speech synthesis.";
speakButton.disabled = true;
stopButton.disabled = true;
} else {
const synth = window.speechSynthesis;
function loadVoices() {
const previousVoice = voiceSelect.value;
voices = synth.getVoices();
voiceSelect.replaceChildren(new Option("Browser default", ""));
for (const voice of voices) {
voiceSelect.add(
new Option(`${voice.name} (${voice.lang})`, voice.voiceURI),
);
}
if (voices.some((voice) => voice.voiceURI === previousVoice)) {
voiceSelect.value = previousVoice;
}
}
loadVoices();
synth.addEventListener("voiceschanged", loadVoices);
speakButton.addEventListener("click", () => {
const transcript = text.value.trim();
if (!transcript) {
status.textContent = "Enter some text first.";
return;
}
// Ignore callbacks from the utterance being replaced.
currentUtterance = null;
synth.cancel();
const utterance = new SpeechSynthesisUtterance(transcript);
const voice = voices.find(
(item) => item.voiceURI === voiceSelect.value,
);
if (voice) {
utterance.voice = voice;
utterance.lang = voice.lang;
} else {
utterance.lang = document.documentElement.lang;
}
utterance.rate = 1;
currentUtterance = utterance;
status.textContent = "Starting speech...";
utterance.onstart = () => {
if (currentUtterance === utterance)
status.textContent = "Speaking...";
};
utterance.onend = () => {
if (currentUtterance !== utterance) return;
currentUtterance = null;
status.textContent = "Finished.";
};
utterance.onerror = (event) => {
if (currentUtterance !== utterance) return;
currentUtterance = null;
status.textContent = `Speech failed: ${event.error}`;
};
synth.speak(utterance);
});
stopButton.addEventListener("click", () => {
currentUtterance = null;
synth.cancel();
status.textContent = "Stopped.";
});
}
</script>
</body>
</html>
The demo limits input to 500 characters to keep the first test short. That’s a demo limit, not a Web Speech API limit. Test longer passages separately on every browser you support.
2. Check voice selection and playback
The voice picker shows a name and language for each available voice. Choose a voice that matches the language of your text; setting utterance.lang does not translate the words.
Speak runs inside a button click rather than on page load. That gives the listener control and avoids relying on autoplay. Pressing Speak again cancels the current request before starting another, rather than adding repeated clicks to the queue. Stop calls cancel(), which removes queued utterances and stops current speech.4
The currentUtterance check keeps a canceled request’s late callback from overwriting the status of a newer request. If you adapt this code into a component, remove the voiceschanged listener when the component unmounts. Cancel playback on unmount only if that component owns the page’s speech queue.
3. Troubleshoot silent or inconsistent speech
| Symptom | What to check |
|---|---|
| The voice picker only shows Browser default | Wait for voiceschanged. If the list stays empty, check browser support and installed system voices. The demo can still request a default voice. |
| No sound after pressing Speak | Check device volume and audio output, try a short sentence in a matching language, and read the status message for a synthesis error. |
| A voice exists on desktop but not mobile | Voice lists are device-dependent. Keep a default option and test on the actual operating systems you support. |
| Repeated clicks cause delayed speech | speak() queues requests. Cancel the old request first if the new text should replace it. |
| Speech fails without an internet connection | The selected voice may use a remote service. Inspect voice.localService and test offline rather than assuming local synthesis. |
For a read-aloud feature, keep the original text visible and make Stop accessible by keyboard. Don’t start speech automatically or treat synthesized speech as a replacement for screen-reader support.
When to use a hosted TTS API
Choose browser speech when the listener’s available voices are sufficient and you only need playback on that device. Choose a hosted API when your application needs generated audio bytes, a provider-managed voice identity, or audio delivered outside the browser.
| Requirement | Browser SpeechSynthesis | Hosted TTS API such as Cartesia |
|---|---|---|
| Credentials | No application API key | API credentials and an account with usage access |
| Voice selection | Browser and operating-system voice list | Provider’s voice library or an authorized custom voice |
| Audio destination | Browser playback | Your application receives audio for playback, storage, or transport |
| Offline behavior | Depends on the voice and device | Cartesia API requests require a network connection |
| Text handling | Local or remote, depending on the voice | Text is sent to the provider; review data requirements before integration |
For Cartesia speech synthesis, call the API from your backend. Keep your API key there, not in a public HTML file or client-side bundle. A browser application can send text to your authenticated backend, which applies input limits and returns audio to the client. Rate-limit that endpoint so visitors cannot spend your API credits without bounds.
Start by generating a WAV file with the Cartesia Python SDK. That guide tests the hosted generation path; it is not a streaming browser player. For a conversational application, you’ll also need playback buffering and interruption handling. Compare that work with Cartesia Managed Agents before building the full voice-agent pipeline yourself.
Related references
Footnotes
-
MDN, SpeechSynthesis. ↩
-
MDN, SpeechSynthesis.getVoices(). ↩
-
MDN, SpeechSynthesis.cancel(). ↩