🚀 ElevenLabs React Integration: Voice UI Now (TTS API)
Learn ElevenLabs React integration for React text to speech: build a voice-enabled UI with a secure TTS API proxy, controls, streaming tips & cost safeguards—now.

Build a Voice-Enabled React UI with ElevenLabs
Voice can make a product easier to use, but adding speech isn't just a matter of calling a text-to-speech API. You also have to decide where the API credentials live, how audio gets to the browser, what users can control, and how to keep an experimental feature from turning into a surprise cost or reliability problem.
ElevenLabs provides a text-to-speech API that turns text into natural-sounding audio. React can handle the UI, and a small server-side endpoint can protect credentials and broker requests. That's a solid starting point for a prototype - and a safer foundation for a real product feature.
This guide covers that architecture, a working Node + React example, the trade-offs between buffering and streaming, and the operational choices that matter once real users start using the feature. The goal isn't "add voice everywhere." It's to find where spoken output actually helps, and ship it without creating avoidable security, accessibility, or cost issues.
Why add voice to a React product?
Voice helps when reading is inconvenient, slow, or inaccessible. It can also add personality to an experience - for example, a game character speaking dialogue, a learning app reading a lesson aloud, or customer support offering spoken responses alongside text.
But voice isn't automatically better. Audio takes time to generate and consume. It can be distracting in a quiet office, annoying in a noisy environment, or frustrating when users can't quickly find a specific sentence (or replay it). In many products, the best design isn't voice-first - it's text-first, with a clear "listen" option.
Start with the user task, not the model
Before you integrate a voice provider, figure out what speech should improve. Ask:
- Is the user consuming long-form content while doing something else?
- Would spoken output make important information more accessible?
- Does the product need expressive dialogue, or is a neutral voice fine?
- Can the user complete the task without audio?
- Is it appropriate to speak this content in the user's environment?
A good first feature has a clear boundary. For example: the user clicks Listen next to a tutorial explanation. The app generates audio for that explanation, shows progress, and displays a player. It's easier to evaluate than automatically speaking every alert, label, and response.
Where ElevenLabs fits
ElevenLabs offers text-to-speech via an API. Your app sends text plus a voice identifier, and the service returns audio. Voice settings affect how the result sounds, but they don't replace product decisions about tone, content, and user control.
Possible use cases include:
- Accessibility and comprehension: Read selected instructions, summaries, or educational content aloud. This should complement semantic HTML and assistive technology support, not replace them.
- Games and interactive stories: Generate dialogue when recording every line would be impractical. Latency and consistency still matter for immersion.
- Customer support: Provide spoken versions of answers, while keeping a readable transcript and a path to a human when needed.
- Personalized assistants: Use a consistent spoken style across interactions, with consent and clear disclosure that voices are synthetic.
The business case should be measurable. Compare task completion, repeat usage, support outcomes, and generation cost. If it sounds great in a demo but makes the main workflow more annoying, it's probably not a product improvement.
Choose an architecture that protects your API key
A common shortcut is to put an ElevenLabs API key in a React "environment variable" and call the provider directly from the browser. That's not secure. In a frontend build, environment variables referenced by the code get bundled into assets that users can inspect. A "hidden" variable name doesn't make it a secret.
The safer pattern looks like this:
- React sends the requested text to an endpoint on your server.
- The server authenticates and validates the request.
- The server calls ElevenLabs using a secret stored in its environment.
- The server returns the audio to the browser.
- React creates a playable object URL and manages its lifecycle.
This adds a small backend responsibility, but it gives you a place to enforce limits, authenticate users, log usage, and switch providers later - without shipping credentials to every client. For broader background, see our guide to API security fundamentals.
Keep the boundary explicit
The browser should send only what's needed for the user's request. The server should decide which provider, model, voice, and settings are allowed. Don't accept an arbitrary voice identifier from an unauthenticated client - otherwise you may enable unwanted usage or access voices outside your intended scope.
At minimum, validate that the text exists, enforce a maximum length, and reject unsupported input. In a real service, also consider user authentication, request rate limits, per-account quotas, and a queue for traffic spikes. Never log API keys. Also think about whether you should log user text - people paste personal, confidential, and sometimes regulated information.
Think about audio delivery honestly
Downloading a complete audio file is the simplest implementation, and for short messages it's often enough. The browser can show a loading state while generation completes, then use its native audio controls.
Streaming can reduce perceived waiting time for long output, but it's not automatic just because you "read chunks." If you buffer the whole response into a Blob before playback, you didn't really stream - you just waited. True progressive playback needs streaming support on both the server and playback side, plus you'll want to test the audio format and behavior on the browsers and devices your users actually use. Don't assume.
For an initial release, measure the full generation time and the user experience. Add streaming when the measured delay is a real problem - not because "streaming" sounds more advanced. This matches other AI product integration decisions: start with the simplest architecture that meets the requirement, then improve based on evidence.
Build a secure ElevenLabs and React implementation
The example below uses a small Express server as a proxy. It keeps the provider key on the server and returns audio to React. Use Node.js 18+ for the built-in fetch API, or use a compatible HTTP client in your backend.
1. Configure the server
Install the server dependencies and store the provider key in a server-side .env file. Don't commit it to version control.
npm install express dotenv
ELEVENLABS_API_KEY=your_server_side_key
ELEVENLABS_VOICE_ID=your_approved_voice_id
PORT=3001
Create a route such as server.js:
import 'dotenv/config';
import express from 'express';
const app = express();
app.use(express.json({ limit: '20kb' }));
app.post('/api/speech', async (req, res) => {
const text = req.body?.text;
if (typeof text !== 'string' || !text.trim()) {
return res.status(400).json({ error: 'Text is required.' });
}
if (text.length > 2000) {
return res.status(413).json({ error: 'Text is too long.' });
}
try {
const voiceId = process.env.ELEVENLABS_VOICE_ID;
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}`,
{
method: 'POST',
headers: {
'xi-api-key': process.env.ELEVENLABS_API_KEY,
'Content-Type': 'application/json',
'Accept': 'audio/mpeg'
},
body: JSON.stringify({
text: text.trim(),
voice_settings: {
stability: 0.75,
similarity_boost: 0.85
}
})
}
);
if (!response.ok) {
const status = response.status === 429 ? 429 : 502;
return res.status(status).json({ error: 'Speech generation is temporarily unavailable.' });
}
const audio = Buffer.from(await response.arrayBuffer());
res.setHeader('Content-Type', 'audio/mpeg');
res.setHeader('Cache-Control', 'no-store');
return res.send(audio);
} catch (error) {
console.error('Speech request failed:', error.message);
return res.status(502).json({ error: 'Could not generate speech.' });
}
});
app.listen(process.env.PORT || 3001);
This buffers the provider response before sending it to the browser, so it's intentionally not streaming. It's a straightforward baseline for short speech requests. Before you expose this to public traffic, add authentication and server-side rate limiting. Also double-check the provider's current API parameters, account limits, and voice permissions - those can change.
2. Call your server from React
Your React component doesn't need the ElevenLabs key. It calls your endpoint, receives audio, and creates a temporary object URL for the player.
import { useEffect, useState } from 'react';
export default function VoiceInput() {
const [text, setText] = useState('');
const [audioUrl, setAudioUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
return () => {
if (audioUrl) URL.revokeObjectURL(audioUrl);
};
}, [audioUrl]);
async function handleSpeak() {
if (!text.trim() || loading) return;
setLoading(true);
setError('');
try {
const response = await fetch('/api/speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error('Speech could not be generated. Try again shortly.');
}
const audioBlob = await response.blob();
setAudioUrl(URL.createObjectURL(audioBlob));
} catch (err) {
setError(err.message || 'Something went wrong.');
} finally {
setLoading(false);
}
}
return (
<section>
<label htmlFor="speech-text">Text to hear</label>
<textarea
id="speech-text"
rows={4}
maxLength={2000}
value={text}
onChange={(event) => setText(event.target.value)}
/>
<button onClick={handleSpeak} disabled={loading || !text.trim()}>
{loading ? 'Generating audio...' : 'Generate speech'}
</button>
{error && <p role="alert">{error}</p>}
{audioUrl && <audio controls src={audioUrl}>Your browser does not support audio playback.</audio>}
</section>
);
}
The effect revokes the object URL when it's replaced or when the component unmounts. Without cleanup, repeated generations can leave browser resources tied up longer than they should.
In a bigger UI, decide what happens if someone submits a second request while the first one is still running: disable the control, cancel the old request, or queue the next one.
Use visible labels, keyboard-accessible controls, and clear progress/error messages. Native <audio controls> is a reasonable default because users already know how it works. If you need a custom player, make sure keyboard operation still works and expose enough state for assistive tech.
3. Treat playback as a product feature
Generation is only part of the interaction. Decide whether audio should autoplay, whether previous results should stay available, and how users can stop or replay audio. For most interfaces, avoid autoplay generated speech - users should choose when it starts.
If the audio corresponds to content that's also on-screen, keep the text visible. That helps users who can't or don't want to listen, makes scanning easier, and gives a fallback if generation fails.
And yes: a speech output feature isn't a replacement for screen-reader support. Semantic structure and accessible interaction patterns still matter.
Make the feature reliable, useful, and responsible
A prototype can succeed with one working request. A production feature needs sensible behavior when requests are slow, users click repeatedly, provider quotas are reached, or the generated audio isn't appropriate for the context.
Control latency and failures
Show progress quickly, but don't pretend audio is ready before it is. Handle network failures, provider errors, and rate limits with a short message and a clear retry path.
A 429 response means you were rate-limited. Retrying every few milliseconds usually makes things worse. If you do retry, use bounded exponential backoff on the server, follow any retry guidance the provider gives you, and don't retry forever.
For repeated or shared content, consider caching - but only if reuse is safe for that content, voice/settings, and user permissions. Avoid caching personal or confidential text by default. Add timeouts and maximum request sizes, and monitor latency, error rates, and usage by account or feature.
Make cost a design constraint
Text-to-speech usage depends on how much text you send and what plan you're on. Pricing and quotas can change, so check the provider's current terms rather than assuming an old quota will hold.
Before launch, estimate:
- How many users will generate audio?
- How much text will each request include?
- Will users regenerate the same passage repeatedly?
- What happens when an account hits its limit?
Then put the controls where they matter: maximum text length, per-user request limits, usage monitoring, and budget alerts. A client-side queue can improve the experience for one user, but it won't protect your API key or enforce global spending limits. Those controls belong on the server.
Consider consent, privacy, and voice identity
Generated speech can sound human, but your product still needs clear rules about whose voice is used and how you describe the feature. If you use a cloned or recognizable voice, make sure you have the rights and consent. Don't imply the output is a real person speaking live when it isn't.
Treat user-submitted text according to its sensitivity. People paste customer details, internal notes, and health information into text boxes. Send only what's needed, explain how you handle data, and don't retain text or audio longer than necessary. If you operate across the US and UK, privacy requirements depend on your data and context - work with legal and security rather than assuming the vendor handles it all.
Validate the business case with a narrow release
Start with one workflow and a small group of users. Measure whether speech improves the task - not just whether people think the voice sounds realistic.
Helpful signals include completion rates, playback starts, abandonment, repeated generations, support requests, and cost per successful task.
If speech helps, expand deliberately. If it doesn't, remove it or redesign it. Voice can be valuable in education, accessibility, gaming, and support - but each context has different tolerance for delay, cost, and synthetic expression. A single default voice and interaction pattern won't fit all of them.
Conclusion
Adding ElevenLabs text-to-speech to a React interface is straightforward technically. The hard part is getting the choices right: keep the API key on the server, make playback user-controlled, preserve text alternatives, and set limits before usage grows.
Start with one clear use case and a buffered audio response. Measure the experience and operating cost. Add streaming, caching, or more advanced voice behavior only when real usage shows a need. It's usually a more reliable path than building a slick demo based on assumptions.
If you're exploring a voice feature, build the server-side proxy first, test it with a short passage, and get feedback from the people who will use it. Then decide whether it's ready for a wider release.