DEV Community

VoiceDeveloper
VoiceDeveloper

Posted on

How to Add Voice to Your React App with ElevenLabs

Why Voice Matters in Modern Web Apps

If you’ve ever built a chatbot, a language‑learning tool, or an accessibility feature, you know how powerful spoken feedback can be. Voice turns a static UI into an interactive experience that feels more human, inclusive, and engaging. With the rise of AI‑generated speech, adding high‑quality text‑to‑speech (TTS) to a React app is easier than ever—no need to spin up a custom neural network or host large audio files.

One service that consistently delivers natural‑sounding voices, low latency, and an easy‑to‑use API is ElevenLabs. In this post we’ll walk through integrating ElevenLabs into a React front‑end, handling authentication, streaming audio, and even a quick demo of voice cloning.

Tip: If you’re curious about the pricing or want a free trial, sign up through this affiliate link: https://try.elevenlabs.io/kr07zfuqn1bp. It supports the article at no extra cost to you.


Overview of the Workflow

  1. Collect user input – a textarea, form field, or any string you want to vocalize.
  2. Send the text to ElevenLabs – using their /v1/text-to-speech/{voice_id} endpoint.
  3. Receive an audio stream – either as a binary audio/mpeg payload or a URL to a temporary file.
  4. Play the audio in the browser – using the HTML5 <audio> element or the Web Audio API.

Because ElevenLabs returns a stream, we can start playback while the rest of the audio is still downloading, keeping the UI snappy.


Setting Up the Project

# Create a new React app (skip if you already have one)
npx create-react-app voice-demo
cd voice-demo

# Install axios for HTTP requests and dotenv for environment variables
npm install axios dotenv
Enter fullscreen mode Exit fullscreen mode

Create a .env file in the root of the project and add your ElevenLabs API key (you’ll get it after signing up):

REACT_APP_ELEVENLABS_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

Important: Never commit .env to source control. Add it to .gitignore.


The Core API Call

ElevenLabs expects a POST request with JSON payload containing the text and optional voice settings. Below is a reusable tts.js helper that returns a Blob you can feed straight into an <audio> tag.

// src/utils/tts.js
import axios from "axios";

const API_KEY = process.env.REACT_APP_ELEVENLABS_API_KEY;
const BASE_URL = "https://api.elevenlabs.io/v1";

export async function synthesizeSpeech(text, voiceId = "EXAVITQu4vr4xnSDxMaL") {
  const url = `${BASE_URL}/text-to-speech/${voiceId}`;

  const response = await axios.post(
    url,
    {
      text,
      // optional: you can tweak stability, similarity, etc.
      // see ElevenLabs docs for all parameters
    },
    {
      headers: {
        "xi-api-key": API_KEY,
        "Content-Type": "application/json",
        Accept: "audio/mpeg",
      },
      responseType: "blob", // important – we want raw audio
    }
  );

  return response.data; // Blob containing MP3
}
Enter fullscreen mode Exit fullscreen mode

The default voiceId above points to the “Rachel” voice, which is a good generic starting point. If you later clone a custom voice, just replace the ID.


Building the UI

Here’s a minimal component that ties everything together:

// src/components/VoiceSynthesizer.jsx
import { useState } from "react";
import { synthesizeSpeech } from "../utils/tts";

export default function VoiceSynthesizer() {
  const [text, setText] = useState("");
  const [audioUrl, setAudioUrl] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const handleSpeak = async () => {
    setLoading(true);
    setError("");
    try {
      const audioBlob = await synthesizeSpeech(text);
      const url = URL.createObjectURL(audioBlob);
      setAudioUrl(url);
    } catch (e) {
      console.error(e);
      setError("Failed to synthesize speech. Check the console.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ maxWidth: "600px", margin: "auto", padding: "2rem" }}>
      <h2>🗣️ Text‑to‑Speech Demo</h2>
      <textarea
        rows={4}
        placeholder="Enter text to speak..."
        value={text}
        onChange={(e) => setText(e.target.value)}
        style={{ width: "100%", marginBottom: "1rem" }}
      />
      <button onClick={handleSpeak} disabled={loading || !text.trim()}>
        {loading ? "Synthesizing…" : "Speak"}
      </button>

      {error && <p style={{ color: "red" }}>{error}</p>}

      {audioUrl && (
        <audio controls autoPlay src={audioUrl} style={{ marginTop: "1rem" }} />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Add the component to App.js:

import VoiceSynthesizer from "./components/VoiceSynthesizer";

function App() {
  return (
    <div className="App">
      <VoiceSynthesizer />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run the app with npm start. Type any sentence, click Speak, and you’ll hear a natural‑sounding voice powered by ElevenLabs.


Voice Cloning: Making the App Personal

One of the coolest features of ElevenLabs is voice cloning—upload a few seconds of a speaker’s voice and get a model that can read any text in that timbre. The flow is:

  1. Create a voice via the /v1/voices endpoint (POST with a short audio sample).
  2. Wait for training (usually a few minutes).
  3. Use the returned voice_id in the TTS request.

Below is a curl example that creates a voice from a local WAV file:

curl -X POST "https://api.elevenlabs.io/v1/voices/add" \
  -H "xi-api-key: YOUR_API_KEY" \
  -H "Content-Type: multipart/form-data" \
  -F "name=MyCustomVoice" \
  -F "samples[]=@/path/to/sample1.wav" \
  -F "samples[]=@/path/to/sample2.wav"
Enter fullscreen mode Exit fullscreen mode

The response contains a voice_id. Plug that ID into the synthesizeSpeech helper:

const customVoiceId = "your_custom_voice_id";
await synthesizeSpeech("Hello, this is my cloned voice!", customVoiceId);
Enter fullscreen mode Exit fullscreen mode

Now the app can speak with a voice that sounds like you—or any brand mascot you upload.


Handling Streaming Audio (Optional Advanced Step)

If you want to start playback before the whole MP3 is downloaded, you can use the Web Audio API with a ReadableStream. ElevenLabs supports Accept: audio/mpeg streaming, so you can pipe the response directly:

export async function streamSpeech(text, voiceId) {
  const response = await fetch(`${BASE_URL}/text-to-speech/${voiceId}`, {
    method: "POST",
    headers: {
      "xi-api-key": API_KEY,
      "Content-Type": "application/json",
      Accept: "audio/mpeg",
    },
    body: JSON.stringify({ text }),
  });

  const context = new AudioContext();
  const source = context.createBufferSource();

  const reader = response.body.getReader();
  const chunks = [];

  // Collect chunks until we have enough to decode
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    // You could decode partial buffers here for true streaming playback
  }

  const blob = new Blob(chunks, { type: "audio/mpeg" });
  const arrayBuffer = await blob.arrayBuffer();
  const audioBuffer = await context.decodeAudioData(arrayBuffer);
  source.buffer = audioBuffer;
  source.connect(context.destination);
  source.start();
}
Enter fullscreen mode Exit fullscreen mode

This pattern is handy for long articles, podcasts, or real‑time narration where latency matters.


Testing and Debugging Tips

Issue Likely Cause Fix
401 Unauthorized Wrong or missing API key Verify REACT_APP_ELEVENLABS_API_KEY and that the key is active.
No audio playback Blob URL not created or revoked early Keep the URL in state until playback finishes (URL.revokeObjectURL after onended).
Voice sounds robotic Using a default voice with low stability settings Pass stability and similarity_boost parameters in the request payload.
CORS errors in dev server Missing mode: "cors" Axios sets it automatically; for fetch add mode: "cors" header.

Deploying to Production

When you push to a static host (Netlify, Vercel, Cloudflare Pages), the environment variable must be prefixed with REACT_APP_ so the build can embed it. Most platforms let you set env vars in the dashboard. Remember to enable HTTPS—ElevenLabs only works over TLS.

If you want to hide the API key completely, consider a tiny serverless function that proxies the request. Here’s a minimal Node.js Express endpoint:

// api/tts-proxy.js (for Vercel Serverless Functions)
import axios from "axios";

export default async function handler(req, res) {
  const { text, voiceId } = req.body;
  const response = await axios.post(
    `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
    { text },
    {
      headers: {
        "xi-api-key": process.env.ELEVENLABS_API_KEY,
        "Content-Type": "application/json",
        Accept: "audio/mpeg",
      },
      responseType: "arraybuffer",
    }
  );

  res.setHeader("Content-Type", "audio/mpeg");
  res.send(response.data);
}
Enter fullscreen mode Exit fullscreen mode

Your front‑end then calls /api/tts-proxy without ever exposing the key.


Wrapping Up

Adding voice to a React app is no longer a research‑paper‑level endeavor. With just a few lines of code you can:

  • Convert any string into natural speech.
  • Clone a custom voice for brand consistency.
  • Stream audio for low‑latency experiences.
  • Keep your API keys safe with serverless proxies.

All of this is powered by ElevenLabs, a service that balances quality, speed, and developer friendliness. If you’re ready to give your users a richer, more accessible experience, give ElevenLabs a spin today.

Try it out now: https://try.elevenlabs.io/kr07zfuqn1bp – you’ll get a free trial, and the link helps support this guide at no extra cost to you. Happy coding!

Top comments (0)