Introduction
If you’ve ever wanted to give your web app a voice—whether it’s reading out notifications, narrating articles, or creating a fully‑spoken chatbot—you’re in the right place. In this walkthrough we’ll combine React with the powerful text‑to‑speech (TTS) and voice‑cloning capabilities of ElevenLabs to build a simple but extensible voice‑enabled UI.
By the end of the article you’ll have:
- A React app that can send text to ElevenLabs and play back the generated audio.
- A reusable
useElevenLabshook that abstracts the API calls. - An example of how to use a custom voice clone for a more personal feel.
Let’s dive in!
Why ElevenLabs?
ElevenLabs has quickly become a go‑to service for developers who need natural‑sounding speech. Their models support multiple languages, fine‑grained control over voice style, and most importantly voice cloning—you can upload a short sample of a speaker’s voice and generate speech that sounds just like them.
The API is straightforward (REST + JSON) and works nicely with browser‑based fetch calls, which means you can keep everything client‑side if you’re okay with exposing your API key in a development build (or you can proxy through your own backend for production).
You can sign up and get a free tier at the official link: https://try.elevenlabs.io/kr07zfuqn1bp.
Getting Started
First, make sure you have Node.js (v16+) installed. Then spin up a fresh React project with Vite—its zero‑config setup is perfect for demos:
npm create vite@latest elevenlabs-react-demo --template react
cd elevenlabs-react-demo
npm install
Add a couple of tiny dependencies we’ll need for audio playback:
npm install --save-dev eslint-plugin-react
npm install howler
Create a .env file in the project root and store your ElevenLabs API key (never commit this file to Git):
VITE_ELEVENLABS_API_KEY=your_api_key_here
Vite automatically exposes variables prefixed with VITE_ to the client code.
Setting Up the ElevenLabs Service
Let’s wrap the raw fetch calls in a tiny utility module so we can reuse it across components.
src/services/elevenlabs.js
// src/services/elevenlabs.js
const API_KEY = import.meta.env.VITE_ELEVENLABS_API_KEY;
const BASE_URL = "https://api.elevenlabs.io/v1";
export async function generateSpeech({
text,
voiceId = "EXAVITQu4vr4xnSDxMaL", // default "Rachel" voice
stability = 0.75,
similarityBoost = 0.75,
}) {
const response = await fetch(`${BASE_URL}/text-to-speech/${voiceId}`, {
method: "POST",
headers: {
"xi-api-key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
voice_settings: {
stability,
similarity_boost: similarityBoost,
},
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error?.message ?? "ElevenLabs request failed");
}
// The API returns raw audio bytes (MP3) when you set `Accept: audio/mpeg`
// but the default response is JSON with a temporary URL. We'll request the URL.
const { audio_url } = await response.json();
return audio_url;
}
If you want to clone a voice, first upload a sample (see ElevenLabs docs) and retrieve the new voiceId. Then pass that ID to generateSpeech.
Building a Reusable Hook
React hooks make it easy to manage async state. Here’s a simple hook that calls our service and returns the audio URL.
src/hooks/useElevenLabs.js
import { useState } from "react";
import { generateSpeech } from "../services/elevenlabs";
export function useElevenLabs() {
const [loading, setLoading] = useState(false);
const [audioUrl, setAudioUrl] = useState(null);
const [error, setError] = useState(null);
const speak = async ({ text, voiceId }) => {
setLoading(true);
setError(null);
try {
const url = await generateSpeech({ text, voiceId });
setAudioUrl(url);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
return { speak, loading, audioUrl, error };
}
Creating the VoiceButton Component
Now we can build a UI component that lets users type a sentence and hear it spoken.
src/components/VoiceButton.jsx
import { useState, useEffect } from "react";
import { useElevenLabs } from "../hooks/useElevenLabs";
import { Howl } from "howler";
export default function VoiceButton({ voiceId }) {
const [text, setText] = useState("");
const { speak, loading, audioUrl, error } = useElevenLabs();
// Play the audio whenever a new URL appears
useEffect(() => {
if (!audioUrl) return;
const sound = new Howl({
src: [audioUrl],
format: ["mp3"],
html5: true,
});
sound.play();
// Clean up when component unmounts or new audio loads
return () => sound.unload();
}, [audioUrl]);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
speak({ text, voiceId });
};
return (
<div style={{ maxWidth: "400px", margin: "auto" }}>
<form onSubmit={handleSubmit}>
<textarea
rows={3}
placeholder="Type something for ElevenLabs to read..."
value={text}
onChange={(e) => setText(e.target.value)}
style={{ width: "100%", padding: "0.5rem" }}
/>
<button
type="submit"
disabled={loading}
style={{
marginTop: "0.5rem",
padding: "0.5rem 1rem",
background: "#0066ff",
color: "#fff",
border: "none",
cursor: loading ? "wait" : "pointer",
}}
>
{loading ? "Speaking…" : "Speak"}
</button>
</form>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
You can now drop <VoiceButton /> anywhere in your app. Pass a custom voiceId if you have a cloned voice; otherwise the default voice works out of the box.
Handling Voice Cloning
Voice cloning adds a personal touch but requires a few extra steps:
-
Create a voice on the ElevenLabs dashboard (or via the
/v1/voicesendpoint). Upload a 30‑second sample of the target speaker. - Capture the returned
voice_id. - Store that ID in your app’s config or fetch it from a backend.
Once you have the ID, just hand it to the hook:
<VoiceButton voiceId="my_custom_voice_id" />
Because the API is the same, you don’t need to change any other code. The generated speech will now sound like the cloned speaker.
Testing the UI
Run the dev server:
npm run dev
Open http://localhost:5173 and you should see a textarea with a “Speak” button. Type something like:
“Hello, Dev community! This is ElevenLabs speaking through React.”
Hit Speak and you’ll hear the audio play back instantly. If you encounter a CORS error, double‑check that your API key is correctly set and that the ElevenLabs endpoint is reachable from your browser.
Tips & Gotchas
| Issue | Fix |
|---|---|
| Audio doesn’t play on mobile | Safari blocks autoplay without user interaction. Our button click counts as an interaction, but ensure you’re not pre‑loading the audio. |
| Rate‑limit errors | Free tier limits the number of requests per minute. Cache generated URLs when possible, or debounce rapid submissions. |
| Voice cloning takes time | After uploading a sample, ElevenLabs may need a few seconds to process. Poll the voice status endpoint until is_ready is true before using the new voice_id. |
| Storing the API key | For production, proxy the request through a serverless function (e.g., Vercel or Cloudflare Workers) to keep the key secret. |
Conclusion
Integrating voice into a web app used to feel like a niche hobby, but with services like ElevenLabs and the simplicity of React hooks, you can add high‑quality speech in just a handful of lines. Whether you’re building an accessibility feature, a chatbot, or a content‑reading tool, the pattern shown here scales: swap out the text, switch the voice ID, and you’re good to go.
Ready to give your UI a voice? Head over to ElevenLabs, grab a free API key, and start experimenting with the code above. Happy coding!
Try ElevenLabs today: https://try.elevenlabs.io/kr07zfuqn1bp.
Top comments (0)