Guides # Guides > Task-focused guides for choosing voices, configuring features, and shipping ResponsiveVoice Task-focused guides. If you are setting up for the first time, start with [Quick Start](/getting-started/quick-start/) instead. ## Deciding [Why ResponsiveVoice?](/guides/why-responsivevoice/)How v2 compares to ElevenLabs, Azure, and Google Cloud for adding speech to a website. [FAQ](/guides/faq/)Pricing, API keys, streaming, browser and Node.js support, available voices. [Browser Support](/guides/browser-support/)Minimum browser and Node.js versions, per package. ## Choosing a voice [Voice Selection](/guides/voice-selection/)Select voices by name, regex, or structured query. [Voice Resolver Hook](/guides/voice-resolver/)Intercept and rewrite voice selectors before they resolve. ## Speaking [Web Player](/guides/web-player/)A drop-in article reader: play controls, paragraph highlighting, mini-player. [App Features](/guides/app-features/)No-code voice features configured from the dashboard, no JavaScript required. [Events](/guides/events/)React to speech starting, ending, pausing, and failing. [Text Chunking](/guides/text-chunking/)How long text is split for synthesis, and when to tune it. ## Machine-readable docs [For AI Agents](/guides/ai-agents/)llms.txt, text bundles, and per-page Markdown for LLMs and coding agents. # For AI Agents > Machine-readable documentation for LLMs and AI agents — llms.txt, full/abridged text bundles, and per-page Markdown. This documentation ships a machine-readable layer for LLMs and AI agents. Every page is available as plain text and Markdown, with curated bundles for whole-site ingestion. ## Agent Skill Teach your coding agent to install and run ResponsiveVoice with one command: ```bash npx skills add responsivevoice/skills ``` It installs into Claude Code, Cursor, GitHub Copilot, Gemini CLI, and other agents that read filesystem [Agent Skills](https://github.com/responsivevoice/skills), covering the browser library, REST API, and language SDKs. ## Endpoints Fetching one topic? Use a section bundle or `/llms-small.txt`. Reserve `/llms-full.txt` for whole-corpus ingestion — it is large and ends with the full generated API reference. [/llms.txt](/llms.txt)Index of the documentation sets, following the llmstxt.org convention. Start here. [Section bundles](/_llms-txt/getting-started.txt)One topic per file: getting-started, guides, sdks, examples, rest-api at /\_llms-txt/.txt. Smallest fetch for a single topic. [/llms-small.txt](/llms-small.txt)Abridged build, API reference removed. Best for conceptual queries on a budget. [/llms-full.txt](/llms-full.txt)The complete documentation as a single text file, including the generated API reference. Large. [/llms.md](/llms.md)A Markdown index of every page. ## Per-page Markdown Append `.md` to any documentation URL to get its Markdown source — for example, `/getting-started/installation.md`. ## Discovery You don’t need to know these URLs in advance — agents and crawlers can find them automatically: * **Every page links to them.** Each page’s `` includes `` tags pointing to `/llms.txt` and `/llms.md`, so fetching any page reveals the machine-readable versions. * **`/llms.txt` sits at the site root**, following the [llmstxt.org](https://llmstxt.org) convention — the standard place agents look first. * **`robots.txt` allows crawling** and lists the sitemap (`/sitemap-index.xml`) and the `llms.*` files. # App Features > Configure the no-code website voice features available from the ResponsiveVoice app. The [ResponsiveVoice app](https://app.responsivevoice.org) lets you turn website voice features on and off without writing custom JavaScript. These features use the same v2 configuration that powers the SDK, so changes made in the app are reflected in the website configuration returned to your installed ResponsiveVoice script. ![The ResponsiveVoice app's website configuration page: site details at the top and the full list of voice-feature toggles below.](/img/app/dashboard.png) [Open the ResponsiveVoice app](https://app.responsivevoice.org)Configure these features for your site — no code required. Note Some features speak on their own — a few, like the welcome message, as soon as the page loads. Browsers don’t allow audio until the visitor interacts with the page, so these play once the visitor responds to ResponsiveVoice’s built-in permission prompt, which captures that first interaction automatically. ## Quick Reference | Feature | What it does | | ----------------------------------------------------- | -------------------------------------------------------------------- | | [Welcome message](#welcome-message) | Speaks a short message after the page loads. | | [Speak selected text](#speak-selected-text) | Speaks text the visitor highlights. | | [Speak links](#speak-links) | Speaks link text when the visitor hovers over a link. | | [Accessibility navigation](#accessibility-navigation) | Speaks interactive elements as the visitor tabs through the page. | | [Paragraph navigation](#paragraph-navigation) | Lets visitors move through readable text with Ctrl+Up and Ctrl+Down. | | [Inactivity message](#inactivity-message) | Speaks after a period without visitor interaction. | | [End-of-page message](#end-of-page-message) | Speaks when the visitor reaches the bottom of the page. | | [Exit-intent message](#exit-intent-message) | Speaks when the visitor moves toward leaving the page. | | [Multiple messages](#multiple-messages) | Rotates between several message variants. | | [Web player](#web-player) | Adds an article reader with highlighting and a mini-player. | ## Welcome Message The welcome message speaks a short greeting or instruction shortly after a visitor opens the page. Use it for a brief introduction, not for long announcements. A good message tells visitors what they can do next, for example: “Welcome. Select any text to hear it spoken aloud.” Configuration notes: * Enable **Welcome message** in the app. * Enter the message text. * Use **Play welcome message once per session** when repeat playback would be annoying. * Preview the message before saving. Troubleshooting: * The message plays once the visitor responds to the permission prompt — browsers don’t allow audio before that first interaction, so it won’t speak the instant the page loads. * Keep the text short so it does not overlap with other automatic messages. ## Speak Selected Text Speak selected text lets visitors highlight text on the page and hear it spoken aloud. This is useful for readers who want help with a sentence, word, product description, or article section without playing the whole page. Configuration notes: * Enable **Speak selected text** in the app. * Save the configuration and reload the website page. * Test by selecting a short piece of visible text. Troubleshooting: * Text inside buttons, forms, scripts, or hidden elements may not be suitable for selection playback. * If a page prevents text selection with CSS or custom JavaScript, this feature may not trigger. ## Speak Links Speak links reads the visible text of a link when a visitor hovers over it. This helps visitors understand where a link goes before activating it. It works best when links have meaningful labels such as “View pricing” rather than vague labels such as “Click here.” Configuration notes: * Enable **Speak links** in the app. * Save and reload the website page. * Hover over a normal text link to test it. Troubleshooting: * Links with no visible text or only decorative icons may not produce useful speech. * For keyboard-first navigation, use [Accessibility navigation](#accessibility-navigation). ## Accessibility Navigation Accessibility navigation speaks interactive elements as visitors move through the page with the Tab key. It is intended for links, buttons, and form controls. The spoken text comes from the element label, accessible name, or visible text. Configuration notes: * Enable **Speak interactive elements using the Tab key** in the app. * Save and reload the website page. * Press Tab through the page and listen to each focused element. Troubleshooting: * If an element has no useful label, improve its visible text, `aria-label`, or associated form label. * Avoid enabling too many automatic speech features on the same page until you have tested the experience. ## Paragraph Navigation Paragraph navigation lets visitors move through readable page content with Ctrl+Up and Ctrl+Down while hearing the current paragraph. This is useful for long-form pages, help articles, and documentation where visitors may want keyboard control over reading. Configuration notes: * Enable **Speak paragraph using CTRL-UP and CTRL-DOWN keys** in the app. * Save and reload the website page. * Test on a content-heavy page with normal paragraphs and headings. Troubleshooting: * Short pages may not have enough readable blocks for this to feel useful. * Pages with unusual markup may need the [Web Player advanced content settings](/guides/web-player/#advanced) instead. ## Inactivity Message The inactivity message speaks after the visitor has not interacted with the page for a period of time. Use it carefully. The best messages are short and helpful, such as “Still there? You can press play to listen to this page.” Configuration notes: * Enable **Inactivity message** in the app. * Enter the message text. * Preview the message before saving. Troubleshooting: * Do not combine long inactivity, welcome, end-of-page, and exit-intent messages without testing the page flow. * If visitors report unexpected audio, shorten the message or disable this feature on quieter pages. ## End-of-Page Message The end-of-page message speaks when the visitor reaches the bottom of the page. Use it as a closing prompt, such as a support reminder, next step, or short call to action. Configuration notes: * Enable **End-of-page message** in the app. * Enter the message text. * Scroll to the bottom of a test page after saving. Troubleshooting: * Infinite-scroll pages or pages with dynamically loaded content may need extra testing. * Keep the message short enough that it does not interrupt normal navigation. ## Exit-Intent Message The exit-intent message speaks when the visitor moves the pointer toward leaving the page. Use it sparingly. It can be helpful for short reminders, but it can also surprise visitors if it is too long or too frequent. Configuration notes: * Enable **Exit-intent message** in the app. * Enter a short message. * Test by moving the pointer toward the top of the browser window. Troubleshooting: * Exit intent is pointer-based and may not apply on all touch devices. * Avoid using this feature for essential information. ## Multiple Messages Message fields can contain several alternatives separated by a pipe character (`|`). ResponsiveVoice chooses one at random when the feature speaks. Example: ```text Welcome to our site.|Need help? Select any text to hear it spoken.|You can listen to this page while you browse. ``` This works for message-style features such as welcome, inactivity, end-of-page, and exit-intent messages. Tips: * Keep each variant short. * Do not put a pipe character inside the message itself. * Preview several times when testing random messages. ## Web Player The web player adds a visible article reader with paragraph highlighting, click-to-jump, and playback controls. This is what your visitors see: ![The web player's pill control: a play button, speed, skip buttons, a progress bar with elapsed and total time, and the ResponsiveVoice brand icon.](/img/web-player/player-controls.png) When the main control scrolls out of view, a floating mini-player keeps playback within reach: ![The floating mini-player docked in the bottom-left corner, with a circular progress ring around the play button.](/img/web-player/mini-player.png) You configure all of this from the **Web Player** panel in the app — no code required. Each section below maps an app setting to what it does. Developers installing or overriding the player in code should use the [Web Player guide](/guides/web-player/) instead. ### Theme Choose a preset — **Neutral** or **ResponsiveVoice** — or set custom colors to match your brand. ![The Theme section of the app's Web Player panel.](/img/web-player/settings-theme.png) ### Layout ![The Layout section of the app's Web Player panel: Position, Width, and Display.](/img/web-player/settings-layout.png) | App field | Default | What it does | | --------- | ------------------------- | ------------------------------------------------------------------------ | | Position | *Before the content* | Before, after, or inline with the content, or inside a custom slot. | | Width | *Shrink to content* | Shrink hugs the controls; Fill spans the container. | | Display | *Block (on its own line)* | Block gives the player its own line; Inline flows with surrounding text. | ### Controls ![The Controls section of the app's Web Player panel: show/hide checkboxes for each control, plus the floating mini-player options.](/img/web-player/settings-controls.png) | App field | Default | What it does | | --------------------- | ------------- | ------------------------------------------------------------- | | Progress | *On* | Shows progress through the readable content. | | Time | *On* | Shows elapsed and total estimated time. | | Skip | *On* | Shows previous and next paragraph buttons. | | Speed | *On* | Shows the speed cycle button. | | Brand | *On* | Shows the ResponsiveVoice brand icon. | | Floating mini-player | *On* | Shows the mini-player when the main player is out of view. | | Mini-player position | *Bottom left* | Places the mini-player in a viewport corner or custom offset. | | Mini-player animation | *Slide* | Uses Slide, Fade, Pop, or None. | ### Behavior ![The Behavior section of the app's Web Player panel: toggles for Highlight paragraphs, Click to jump, and Skip code & hidden content.](/img/web-player/settings-behavior.png) | App field | Default | What it does | | -------------------------- | ------- | ------------------------------------------------------------------------------------------- | | Highlight paragraphs | *On* | Highlights the currently spoken element. | | Click to jump | *On* | Lets visitors click a paragraph to start reading there. | | Skip code & hidden content | *On* | Excludes scripts, styles, form controls, embedded media, and hidden content from narration. | ### Advanced ![The Advanced section of the app's Web Player panel: Content container, Paragraphs to read, Voice override, and Exclude from narration.](/img/web-player/settings-advanced.png) | App field | Default | What it does | | ---------------------- | ----------------- | -------------------------------------------------------------------- | | Content container | *article* | CSS selector for the element that contains readable content. | | Paragraphs to read | *p, h2, h3, li* | CSS selector for the readable elements inside the container. | | Voice override | *Website default* | Optional voice for this player only. | | Exclude from narration | *empty* | Extra CSS selectors to skip, in addition to the built-in exclusions. | ### Troubleshooting #### The player doesn’t appear Confirm the **Web Player** toggle is on and your ResponsiveVoice script is installed on the page. If you set a **Content container**, make sure it matches an element that exists on the page. #### The player is in the wrong place Adjust **Position** under Layout. If your theme wraps the whole page in one article, point **Content container** at the specific content area instead. #### The wrong text is read, or paragraphs are missing Check that **Paragraphs to read** matches your content, and that nothing you want read is being removed by **Skip code & hidden content** or **Exclude from narration**. #### The mini-player doesn’t appear Confirm **Floating mini-player** is on, then scroll until the main player leaves view while it is playing. ### Live preview and testing Use the app preview to confirm the experience before saving, then test once on the real site too: * Press play and confirm the selected voice sounds right. * Confirm the progress, time, skip, speed, and brand controls match the Controls panel. * Click a paragraph if **Click to jump** is enabled. * Confirm highlighting appears if **Highlight paragraphs** is enabled. * Scroll until the main player leaves view and confirm the mini-player appears if enabled. * Add a known excluded element and confirm it is skipped. ### Advanced Usage Need to install, theme, or override the player in code? The developer guide covers the programmatic path with `init()` and `mount()`. [Web Player guide (for developers)](/guides/web-player/)Configure and override the web player programmatically with init() and mount(). # Browser Support > Browser and platform compatibility for ResponsiveVoice ResponsiveVoice targets modern evergreen browsers (released \~2020 onward), with automatic fallback. Base text-to-speech needs the Web Speech API or a `fetch`-capable runtime; some features require newer browsers — see [Feature requirements](#feature-requirements). Support is shown per delivery method. The **browser bundle (CDN)** is Babel + core-js polyfilled, so it reaches the widest range; **`@responsivevoice/core`** and **`@responsivevoice/api-client`** are raw npm packages whose minimum versions reflect the browser APIs they use — your bundler supplies any transpilation. [Browser bundle (CDN)](/getting-started/quick-start/) * ![Chrome](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome69+ * ![Firefox](/_astro/firefox.1bWoP6pv_Zks0Xz.svg)Firefox65+ * ![Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)Safari12+ * ![Edge](/_astro/edge.HDH_c98u_ZkUqCK.svg)Edge79+ * ![iOS Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)iOS Safari12+ * ![Chrome Android](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome Android69+ * ![Android WebView](/_astro/android-webview_64x64._FpYT5CQ_1iPoo.webp)Android WebView69+ Polyfilled via Babel + core-js — the widest supported tier. [@responsivevoice/core](/getting-started/installation/#core-package) * ![Chrome](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome69+ * ![Firefox](/_astro/firefox.1bWoP6pv_Zks0Xz.svg)Firefox65+ * ![Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)Safari14+ * ![Edge](/_astro/edge.HDH_c98u_ZkUqCK.svg)Edge79+ * ![iOS Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)iOS Safari14+ * ![Chrome Android](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome Android69+ * ![Android WebView](/_astro/android-webview_64x64._FpYT5CQ_1iPoo.webp)Android WebView69+ [@responsivevoice/api-client](/sdks/typescript/) * ![Chrome](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome66+ * ![Firefox](/_astro/firefox.1bWoP6pv_Zks0Xz.svg)Firefox65+ * ![Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)Safari12+ * ![Edge](/_astro/edge.HDH_c98u_ZkUqCK.svg)Edge79+ * ![iOS Safari](/_astro/safari.na3_-uQk_Z1PHqby.svg)iOS Safari12+ * ![Chrome Android](/_astro/chrome.CRByiUFQ_Z2pJKxj.svg)Chrome Android66+ * ![Android WebView](/_astro/android-webview_64x64._FpYT5CQ_1iPoo.webp)Android WebView66+ *Minimum versions are generated from the APIs each package actually uses ([MDN Browser Compat Data](https://github.com/mdn/browser-compat-data)). npm packages are raw and unpolyfilled; the bundle's reflect its Babel + core-js polyfills.* Note These npm minimum versions are raw and unpolyfilled. To support older browsers, load the CDN bundle (already polyfilled) or add polyfills in your build — see [Polyfilling for older browsers](#polyfilling-for-older-browsers). Tip `@responsivevoice/types` is pure TypeScript with no browser runtime requirements. ## Feature requirements Some capabilities need newer browsers than the base minimum. All are degradable — when the API is missing, ResponsiveVoice falls back automatically. | Feature | Requires | Minimum | | -------------------------- | ----------------- | -------------------------------------------------- | | HTTP/WebSocket streaming | MediaSource | iOS Safari 13+ | | Web player (in-page UI) | Shadow DOM | Firefox 63+, Edge 79+ | | Native (Web Speech) voices | `speechSynthesis` | unavailable in Android WebView (falls back to API) | Note Firefox has limited native voice support. ResponsiveVoice automatically uses the fallback API when needed. ## Polyfilling for older browsers The npm packages ship modern JavaScript and leave transpilation to your bundler — the common convention. To support older browsers, add [core-js](https://github.com/zloirock/core-js) through [@babel/preset-env](https://babeljs.io/docs/babel-preset-env) with a browserslist target: babel.config.js ```js module.exports = { presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: '3' }]], }; ``` This polyfills JavaScript built-ins like `URLSearchParams` and `Promise`. DOM and platform APIs (Shadow DOM, MediaSource) can’t be polyfilled — those features need their listed browsers, but base text-to-speech still works without them. ## How Fallback Works ![Diagram](/d2/docs/guides/browser-support-0.svg) ## Native vs Fallback | Feature | Native (Web Speech API) | Fallback (API) | | --------------- | ----------------------- | -------------- | | Latency | Instant | \~100–300 ms\* | | Offline | | | | Voice quality | Varies by OS | Consistent | | Boundary events | | | | SSML support | | | Note \* Fallback synthesis typically returns audio in \~100–300 ms; cached phrases in under 100 ms. Longer or uncached requests may take up to \~700 ms. These figures are indicative, not a guarantee. ## Platform-Specific Notes ### Chrome / Chromium * Best voice selection with Google TTS voices * Voices like “Google UK English Female” available * Full boundary event support * May require user interaction to start audio ### Safari / iOS * Apple voices with “(Enhanced)” variants * Good offline voice quality * iOS requires user gesture to begin playback * Some voices locked to specific iOS versions ### Firefox * Limited native voice support * Falls back to API more frequently * No boundary events when using fallback * Full functionality with API fallback ### Edge * Windows voices available (David, Zira, etc.) * Good Web Speech API support * Microsoft neural voices on Windows 10+ ## Mobile Considerations ### iOS ```typescript // Optional: trigger speech from your own button instead of the built-in prompt document.getElementById('speakBtn').addEventListener('click', () => { rv.speak('Hello from iOS', 'UK English Female'); }); ``` Tip Mobile browsers (iOS, and Android Chrome) block autoplay, so audio needs a user gesture — but ResponsiveVoice handles this automatically with a built-in permission prompt that captures the first tap and unlocks audio. You can disable it and drive speech from your own UI (like the button above). See [the FAQ](/guides/faq/#does-responsivevoice-work-on-ios). ### Android * Android Chrome requires a user gesture before audio — handled automatically by the same built-in permission prompt as iOS * Voice availability varies by device manufacturer * Google TTS voices commonly available * May need to download voices in system settings ### Android WebView Caution The Web Speech API (native TTS) is **not supported** in Android WebView. ResponsiveVoice automatically falls back to the API for all speech synthesis. * Minimum supported: Android WebView 66+ * Native TTS unavailable - always uses API fallback * Audio playback works normally via Audio element * Common in hybrid apps (Cordova, Capacitor, React Native WebView) ## Node.js Support ResponsiveVoice works in Node.js environments using the [`@responsivevoice/api-client`](/getting-started/installation/#api-client) package. The minimum Node.js version varies by package: | Package | Minimum Node.js | | -------------------------------- | --------------- | | `@responsivevoice/types`, `text` | 14+ | | `@responsivevoice/api-client` | 16+ | `@responsivevoice/core` and `@responsivevoice/features` target the browser — Node is only their build environment (bundle with Vite/webpack), not a runtime. ### Node.js 18+ (recommended) All features work out of the box with native `fetch`, `Blob`, and `AbortController`: ```typescript import { ResponsiveVoiceAPIClient } from '@responsivevoice/api-client'; const client = new ResponsiveVoiceAPIClient({ apiKey: process.env.RESPONSIVEVOICE_API_KEY, apiSecret: process.env.RESPONSIVEVOICE_API_SECRET, }); const audio = await client.synthesize({ text: 'Hello from Node.js', voice: 'UK English Female', }); ``` ### Node.js 16–17 Native `fetch` is not available. Pass a fetch implementation via the `fetch` config option: ```typescript import fetch from 'node-fetch'; import { ResponsiveVoiceAPIClient } from '@responsivevoice/api-client'; const client = new ResponsiveVoiceAPIClient({ apiKey: process.env.RESPONSIVEVOICE_API_KEY, apiSecret: process.env.RESPONSIVEVOICE_API_SECRET, fetch, }); ``` ### WebSocket streaming on Node.js < 22 The global `WebSocket` was added in Node.js 22. On older versions, pass a WebSocket implementation: ```typescript import WebSocket from 'ws'; import { WebSocketConnection } from '@responsivevoice/api-client'; const ws = new WebSocketConnection({ baseUrl: 'https://texttospeech.responsivevoice.org', apiKey: process.env.RESPONSIVEVOICE_API_KEY, WebSocket, }); ``` ## Feature Detection Check platform capabilities: ```typescript // Check if the Web Speech API is available const hasNativeTTS = 'speechSynthesis' in window; // Whether ResponsiveVoice can use native voices on this platform const nativeSupported = rv.isNativeSupported(); // Check if a voice name is in the resolvable catalog const voices = rv.getVoices(); const hasUKFemale = voices.some((v) => v.name === 'UK English Female'); // Native <-> fallback switches are reported via the OnServiceSwitched event rv.addEventListener('OnServiceSwitched', (payload) => { console.log(`Switched from ${payload.from} to ${payload.to}`); }); ``` ## Forcing Fallback Force server (fallback) audio even when native voices exist — set it at init or toggle at runtime: ```typescript // At init const rv = await getResponsiveVoice({ apiKey: 'YOUR_API_KEY', forceFallback: true, }); // Or at runtime rv.setForceFallback(true); ``` ## User Interaction Requirements Modern browsers require user interaction before playing audio: ```typescript // ❌ This may be blocked window.onload = () => { rv.speak('Hello'); // Blocked by autoplay policy }; // ✅ This works button.onclick = () => { rv.speak('Hello'); // Allowed - user initiated }; ``` ### Workaround: Initialize on First Interaction On mobile, ResponsiveVoice’s built-in permission prompt already unlocks audio on the first gesture — use this manual pattern only if you’ve disabled the prompt and want to unlock from your own handler: ```typescript let initialized = false; document.addEventListener( 'click', () => { if (!initialized) { rv.speak('', 'UK English Female'); // Silent init initialized = true; } }, { once: true }, ); ``` ## Testing Across Platforms ```typescript // Log platform info for debugging console.log({ browser: navigator.userAgent, hasNativeTTS: 'speechSynthesis' in window, nativeVoices: window.speechSynthesis?.getVoices().length ?? 0, rvVoices: rv.getVoices().length, }); ``` # Events > Handling speech events and callbacks ## Event System ResponsiveVoice gives you two ways to observe speech: * **Per-call callbacks** — passed inline to `speak()`, bound to that one utterance, and reset on the next `speak()`. * **Global events** — registered once with `addEventListener()`, firing across every `speak()` call. Tip Try the [**live Events & Callbacks demo**](https://examples.responsivevoice.org/browser/events-callbacks/) — every per-call callback and global event lights up as you click Speak. The companion [docs page](/examples/events-callbacks) explains the design. ## Per-call callbacks Pass these in the options object of `speak(text, voice?, options?)`. They belong to a single utterance. | Callback | Signature | Fires | | ------------ | ------------------------------------------- | ---------------------------------------------- | | `onstart` | `() => void` | speech begins | | `onend` | `() => void` | speech completes | | `onerror` | `(error: Error) => void` | an error occurs | | `onboundary` | `(charIndex: number, name: string) => void` | crosses a word/sentence boundary (native only) | ```typescript rv.speak('Hello world', 'UK English Female', { onstart: () => console.log('Started speaking'), onend: () => console.log('Finished speaking'), onerror: (error) => console.error('Error:', error.message), onboundary: (charIndex, name) => { console.log(`Boundary (${name}) at character ${charIndex}`); }, }); ``` Note `onboundary` only fires with native browser voices, not the fallback (server) audio engine. ## Global events Register once with `addEventListener(name, handler)`; they fire across every `speak()` call. Event names are PascalCase: | Event | Payload | Fires | | ---------------------- | --------------------------------- | ------------------------------------------- | | `OnReady` | — | client initialized and ready | | `OnLoad` | — | alias of `OnReady` (legacy) | | `OnStart` | — | an utterance starts | | `OnEnd` | — | an utterance ends | | `OnPause` | — | speech is paused | | `OnResume` | — | speech resumes | | `OnError` | `{ error, message? }` | an error occurs | | `OnVoiceResolved` | voice-resolution details | a voice is resolved for an utterance | | `OnServiceSwitched` | `{ from, to }` | engine switches between native and fallback | | `OnPartStart` | `{ partIndex, totalParts, text }` | a text chunk starts speaking | | `OnPartEnd` | `{ partIndex, totalParts, text }` | a text chunk finishes | | `OnClickEvent` | — | a user gesture (click) is detected | | `OnAllowSpeechClicked` | `{ allowed }` | user responds to the permission prompt | ```typescript rv.addEventListener('OnStart', () => console.log('Speech started')); rv.addEventListener('OnPause', () => console.log('Speech paused')); rv.addEventListener('OnResume', () => console.log('Speech resumed')); ``` Pause and resume are **only** global events — they are not per-call callbacks. See the [`RVEventType` reference](/api/types/src/#rveventtypeschema) for the complete list of event names. ### Removing listeners Pass the same handler reference you registered: ```typescript const handler = () => console.log('Speech started'); rv.addEventListener('OnStart', handler); rv.removeEventListener('OnStart', handler); ``` ## Error handling The per-call `onerror` callback receives a standard [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). The `OnError` event delivers a payload `{ error, message? }` whose `error` is that same `Error`: ```typescript rv.speak('Hello', 'UK English Female', { onerror: (error) => { console.error('Speech failed:', error.message); }, }); ``` ## Async/await pattern Wrap `speak()` in a Promise using `onend` and `onerror`: ```typescript function speakAsync(text: string, voice: string): Promise { return new Promise((resolve, reject) => { rv.speak(text, voice, { onend: () => resolve(), onerror: (error) => reject(error), }); }); } // Usage async function readParagraphs(paragraphs: string[]) { for (const paragraph of paragraphs) { await speakAsync(paragraph, 'UK English Female'); } console.log('All paragraphs read'); } ``` ## Progress tracking UI Use `onboundary` to update a progress bar as speech advances (native voices only): ```typescript let startTime: number; rv.speak(longText, 'UK English Female', { onstart: () => { startTime = Date.now(); progressBar.style.width = '0%'; }, onboundary: (charIndex) => { const progress = (charIndex / longText.length) * 100; progressBar.style.width = `${progress}%`; }, onend: () => { progressBar.style.width = '100%'; console.log(`Completed in ${Date.now() - startTime}ms`); }, }); ``` ## Queue events Each `speak()` call carries its own callbacks: ```typescript rv.speak('First sentence', 'UK English Female', { onend: () => console.log('First done, starting second'), }); rv.speak('Second sentence', 'UK English Female', { onstart: () => console.log('Second starting'), onend: () => console.log('Queue complete'), }); ``` # Frequently Asked Questions > Common questions about ResponsiveVoice — pricing, browser and Node.js support, API keys, streaming, and available voices. ResponsiveVoice is TypeScript-first text-to-speech for browsers and Node.js. Answers to the questions we hear most often are below. ## Is ResponsiveVoice free to use? Yes. The ResponsiveVoice library is open source and free to use from npm or a CDN. Create a free account at [responsivevoice.org/register](https://responsivevoice.org/register) — it takes a few seconds — to unlock free server voices for your site. Without an account the library runs in demo mode. Paid plans add features such as streaming, and premium voice providers (Microsoft Azure, OpenAI, Google Cloud) are supported via Bring Your Own Key (BYOK). ## Which browsers and runtimes are supported? ResponsiveVoice runs in all evergreen browsers and in Node.js, using the native Web Speech API where available and server voices (with an account) otherwise. See the [Browser Support](/guides/browser-support/) guide for the full compatibility matrix. ## Do I need an API key? Yes, to use server voices — and it’s free. Register an account to get one. The key is a website identity, not a secret: it’s tied to your registered domain, so it’s safe to include in client-side code. Without a key, the library runs in demo mode. ## Does ResponsiveVoice support streaming audio? Yes — on higher-tier plans. Audio is delivered as it’s synthesized via HTTP audio streaming or WebSocket streaming, so playback can start before the full clip is ready. Other tiers return the complete audio in a single response. ## How many voices and languages are available? The base catalog includes 100+ voices across 50+ languages and genders, chosen through the [voice resolution chain](/guides/voice-selection/) (native Web Speech or fallback). Bring Your Own Key (BYOK) providers add their own voices on top — growing the catalog to thousands. ## Does ResponsiveVoice work on iOS? Yes. iOS (and some mobile browsers) require a user gesture before audio can play, and ResponsiveVoice handles that automatically — it shows a built-in permission prompt that captures the first tap and unlocks audio, so you don’t need to add your own button. The prompt is customizable, or you can disable it and trigger speech from your own UI instead. ## How can I improve speech quality? Punctuation shapes pacing and emphasis — add commas and periods for natural pauses. For tricky pronunciations, respell a word phonetically, add hyphens between syllables, or spell it out letter by letter. You can also configure text replacements for consistent pronunciation of names and domain terms. ## Can I change the speaking rate, pitch, and volume? Yes. Set `rate` and `pitch` (0–2, default 1) and `volume` (0–1, default 1) per request. Native browser voices (Web Speech API) apply them directly; for voices that synthesize server-side — API-only voices, or when the browser lacks the requested voice — how each adjustment applies depends on that provider. ## Does ResponsiveVoice support SSML? SSML (voice markup) is part of the Web Speech API specification, but no current browser actually implements it, and there’s no announced commitment to add it. So ResponsiveVoice takes plain text — shape delivery with the `rate`, `pitch`, and `volume` parameters, plus punctuation and text replacements for pacing and pronunciation. If browsers add SSML support, ResponsiveVoice will adopt it. # Text Chunking > How ResponsiveVoice handles long text ## Overview ResponsiveVoice automatically splits long text into smaller chunks for optimal playback. This ensures reliable speech synthesis even with large amounts of text. ## Why Chunking? * **API Limits**: TTS services have character limits per request * **Memory**: Smaller audio buffers are more efficient * **Responsiveness**: Speech starts faster with smaller chunks * **Reliability**: Reduces chance of timeouts and errors ## Automatic Chunking Chunking happens automatically when you call `speak()`: ```typescript // This long text is automatically split into chunks responsiveVoice.speak( ` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. `, 'UK English Female', ); ``` ## Chunk Boundaries Within the character limit, text is split at the latest (rightmost) natural boundary, preferring higher-priority delimiters first: 1. **Sentence endings** (`.` `?` `!`) 2. **Major separators** (`;` `:`) 3. **Clause separators** (`,`) 4. **Word boundaries** (spaces) 5. **CJK character boundaries** (between ideographs, when no earlier boundary fits) Tip The chunker never splits in the middle of a word, and skips `.`/`,` inside numbers like `3.14` or `1,000`. ## Default Limits | Setting | Value | Description | | ------------------ | --------- | -------------------------------------- | | Default chunk size | 200 chars | Characters per chunk when not set | | Min chunk size | 50 chars | Lower bound (smaller values clamp up) | | Max chunk size | 300 chars | Upper bound (larger values clamp down) | | Sentence priority | High | Prefers sentence boundaries | These apply to voices that play on the device. Voices served by the API use a larger chunk that `characterLimit` does not lower. ## Custom Configuration The chunk character limit is a page-wide setting. Set it globally: * Browser bundle (CDN) ```html ``` * npm (ESM) ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; // At startup const rv = await getResponsiveVoice({ characterLimit: 150 }); // clamped to 50–300 // Or at runtime rv.setCharacterLimit(150); rv.getCharacterLimit(); // 150 ``` Note Examples below use the browser bundle’s global `responsiveVoice`. With the npm package, call the same methods on the instance returned by `getResponsiveVoice()`. ## Seamless Playback Chunks are queued and played sequentially without gaps: ```typescript responsiveVoice.speak(longArticle, 'UK English Female', { onstart: () => console.log('Started reading article'), onend: () => console.log('Finished reading article'), // onend fires once when ALL chunks complete }); ``` ## Manual Chunking For complete control, split the text yourself with `chunkText()`. Each chunk is `{ text, index, total, isLast }`: ```typescript const chunks = responsiveVoice.chunkText(article, { characterLimit: 200 }); for (const chunk of chunks) { await new Promise((resolve) => responsiveVoice.speak(chunk.text, 'UK English Female', { onend: resolve }), ); } ``` `chunkText` is also importable standalone from `@responsivevoice/text`. ## Progress with Chunks Track progress across chunks: ```typescript const chunks = responsiveVoice.chunkText(longText, { characterLimit: 200 }); let currentChunk = 0; function speakNextChunk() { if (currentChunk >= chunks.length) { console.log('Complete!'); return; } const chunk = chunks[currentChunk]; const progress = ((chunk.index + 1) / chunk.total) * 100; console.log(`Progress: ${progress.toFixed(0)}%`); responsiveVoice.speak(chunk.text, 'UK English Female', { onend: () => { currentChunk++; speakNextChunk(); }, }); } speakNextChunk(); ``` ## Special Characters The chunker is aware of: * **Numbers**: won’t split on `.` or `,` inside a number (`3.14`, `1,000`) * **Grouping pairs**: prefers not to split inside quotes, parentheses, or brackets ```typescript // Numbers stay intact responsiveVoice.speak('The price is $19.99 per month.', voice); // Quoted text is kept together responsiveVoice.speak('She said "Hello there, friend!" and waved.', voice); ``` ## SSML ResponsiveVoice takes **plain text**, not SSML — no current browser implements the Web Speech API’s SSML, so it isn’t supported. Shape delivery with the `rate`, `pitch`, and `volume` parameters plus punctuation. See the [FAQ](/guides/faq/#does-responsivevoice-support-ssml). ## Performance Tips 1. **Pre-chunk large texts** for better control 2. **Keep `characterLimit` within range** (50–300; default 200) 3. **Avoid very small chunks** (causes choppy playback) 4. **Consider caching** for repeated content # Voice Resolver Hook > Intercept and transform voice selectors before resolution ## Overview The `resolveVoice` hook lets integrating applications intercept the voice selector passed to `speak()` and transform it before the voice matching chain runs. This is useful when an external system passes voice names that don’t match ResponsiveVoice’s voice catalogue — instead of modifying every call site, a single hook can remap, normalize, or redirect selectors. ## Usage ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; const rv = await getResponsiveVoice({ apiKey: 'YOUR_KEY', resolveVoice: (selector) => { if (typeof selector === 'string') { const aliases: Record = { 'Google UK English Female': 'UK English Female', 'Microsoft Zira': 'US English Female', }; return aliases[selector] ?? selector; } return selector; }, }); // "Google UK English Female" is silently remapped to "UK English Female" rv.speak('Hello', 'Google UK English Female'); ``` ## Hook Signature ```typescript type ResolveVoiceHook = ( selector: VoiceSelector | undefined, ) => VoiceSelector | undefined; ``` | Parameter | Type | Description | | ----------- | ---------------------------- | ----------------------------------------------------------------------- | | `selector` | `VoiceSelector \| undefined` | The incoming voice selector, or `undefined` when no voice was specified | | **Returns** | `VoiceSelector \| undefined` | A transformed selector, or `undefined` to use `defaultVoice` | `VoiceSelector` is a union of three forms (the post-parse output the hook receives): | Type | JS form | Wire form | Meaning | | --------------- | --------------------- | ------------------------------------ | -------------------------------- | | `string` | `'UK English Female'` | same | Resolve by exact voice name | | `RegexSelector` | `/Portuguese/` | `{ regex: 'Portuguese', flags: '' }` | First voice matching the pattern | | `VoiceQuery` | `{ lang: 'pt' }` | same | Structured filter (AND logic) | The hook receives the post-parse `VoiceSelector`, where any incoming JS `RegExp` has already been normalized to the `{ regex, flags }` literal form. The hook’s return value can be either form — a returned `RegExp` is normalized the same way before reaching the resolver. ## Return Value Semantics | Hook returns | What happens | | ----------------------------- | ----------------------------------------------------- | | A `string` | Resolves by name (exact match, then fallback chain) | | A `RegExp` or `RegexSelector` | Resolves by pattern (first match) | | A `VoiceQuery` | Resolves by structured query (lang, gender, provider) | | `undefined` | Falls through to the configured `defaultVoice` | Caution The hook does **not** fire when `params.voice` is set. That escape hatch passes a raw `SpeechSynthesisVoice` object and bypasses the entire resolution chain, including the hook. ## Patterns ### Static aliasing Map legacy or external voice names to ResponsiveVoice names: ```typescript resolveVoice: (selector) => { if (typeof selector === 'string') { const map: Record = { 'old-voice-name': 'UK English Female', 'legacy-male': 'US English Male', }; return map[selector] ?? selector; } return selector; }, ``` ### Locale-based routing Redirect to a locale-appropriate voice when the exact name doesn’t exist: ```typescript resolveVoice: (selector) => { if (typeof selector === 'string' && !knownVoices.has(selector)) { const locale = extractLocale(selector); const gender = extractGender(selector); return { lang: locale, gender }; } return selector; }, ``` ### Debug logging Observe what selectors are being passed without changing behavior: ```typescript resolveVoice: (selector) => { console.log('[RV] resolving voice:', selector); return selector; }, ``` ## TypeScript Both `ResolveVoiceHook` and `VoiceSelector` are exported from `@responsivevoice/core`: ```typescript import type { ResolveVoiceHook, VoiceSelector } from '@responsivevoice/core'; const myHook: ResolveVoiceHook = (selector) => { // your logic return selector; }; ``` # Voice Selection > How to select voices using names, patterns, and structured queries The second argument to `speak()` is a **voice selector** — a `string`, `RegExp`, or structured query that tells ResponsiveVoice which voice to use: ```typescript rv.speak(text: string, voice?: VoiceSelectorInput, params?: SpeakOptions): void; ``` `VoiceSelector` (the post-parse, on-the-wire form) is a union of three JSON-serializable shapes: | Type | JS form | Wire form | Description | | --------------- | --------------------- | ------------------------------------- | -------------------------------- | | `string` | `'UK English Female'` | same | Exact voice name | | `RegexSelector` | `/Portuguese/i` | `{ regex: 'Portuguese', flags: 'i' }` | First voice matching the pattern | | `VoiceQuery` | `{ lang: 'pt' }` | same | Structured filter (AND logic) | In JS code, pass a real `RegExp` literal and the schema normalizes it to the JSON-clean `{ regex, flags }` form on parse — the resolver, server payloads, and SDKs all see the wire form, so the contract is identical across every language. ## By Name The simplest form — pass a ResponsiveVoice voice name as a string: ```typescript rv.speak('Hello', 'UK English Female'); rv.speak('Hello', 'US English Male'); rv.speak('Bonjour', 'French Female'); rv.speak('Hallo', 'Deutsch Male'); ``` If the exact name isn’t available on the current platform, the [voice matching chain](#voice-matching-chain) kicks in to find the closest alternative. ## By Pattern Pass a `RegExp` to match against all non-deprecated voice names. The first match wins: ```typescript rv.speak('Olá', /Portuguese/); rv.speak('Hello', /English.*Female/i); ``` In server-side config or non-JS SDKs (Python, Go, PHP, Java), use the JSON literal form instead — it’s the same selector after the schema normalizes: ```json { "regex": "Portuguese" } { "regex": "English.*Female", "flags": "i" } ``` Tip Pattern matching is useful when you want any voice from a family (e.g., any Portuguese variant) rather than a specific voice name. ## By Query Pass a `VoiceQuery` object to filter voices by attributes. All conditions are AND-ed — a voice must match every specified field: | Field | Type | Behavior | | ---------- | ---------------------------------- | --------------------------------------------------- | | `name` | `string` | Case-insensitive; exact match first, then substring | | `lang` | `string` | BCP-47 prefix match (`"pt"` matches `"pt-BR"`) | | `gender` | `'f' \| 'm' \| 'male' \| 'female'` | Gender filter | | `isByok` | `boolean` | Filter to BYOK (Bring Your Own Key) voices only | | `provider` | `string` | Provider name, case-insensitive | ```typescript // By language rv.speak('Bonjour', { lang: 'fr' }); // By language + gender rv.speak('Olá', { lang: 'pt', gender: 'f' }); // By provider (BYOK voices) rv.speak('Hello', { provider: 'Google Cloud WaveNet', lang: 'en-GB', gender: 'm', }); ``` ## Direct Voice Override When you have a raw `SpeechSynthesisVoice` object from the browser’s Web Speech API, you can pass it directly via the `params.voice` option: ```typescript const nativeVoices = speechSynthesis.getVoices(); const samantha = nativeVoices.find((v) => v.name === 'Samantha'); rv.speak('Hello', undefined, { voice: samantha }); ``` Caution This bypasses the entire ResponsiveVoice resolution chain, including the [`resolveVoice` hook](/guides/voice-resolver/). The voice must be a valid `SpeechSynthesisVoice` from the current browser. ## Default Voice When no voice selector is passed to `speak()`, the configured default voice is used. The built-in default is `'UK English Female'`. ```typescript // Set at init const rv = await getResponsiveVoice({ apiKey: 'YOUR_KEY', defaultVoice: 'US English Female', }); rv.speak('Uses US English Female'); // Change at runtime rv.setDefaultVoice('French Male'); rv.speak('Uses French Male now'); ``` ## Language-Aware Website Widgets ResponsiveVoice accepts a language query, but your interface decides when to change languages. A multilingual website widget should normally use this precedence: 1. A voice explicitly chosen by the visitor 2. Language detected from text the visitor typed 3. The browser’s preferred language 4. English fallback using `UK English Female` ```typescript function browserLanguage(): string { return (navigator.languages?.[0] || navigator.language || 'en') .toLowerCase() .split('-')[0]; } function selectorForLanguage(language: string) { const lang = language.toLowerCase().split('-')[0]; // Keep the English experience consistent and premium by default. if (lang === 'en') return 'UK English Female'; return { lang, gender: 'f' } as const; } function speakVisitorText(text: string, selectedVoice?: string) { // Supply this with your preferred client-side or server-side detector. const typedLanguage = text.length >= 20 ? detectLanguage(text) : undefined; const language = typedLanguage || browserLanguage(); const voice = selectedVoice || selectorForLanguage(language); rv.speak(text, voice); } ``` Use browser language to initialize placeholder text and the first suggested voice. Re-run language detection after the visitor types enough text to make a useful decision, but do not override a manual voice selection. Note The SDK does not guess the language of arbitrary text. Keep detection in the application so you can choose the detector, confidence threshold, privacy behavior, and fallback policy that fit your site. ## Force Fallback Set `forceFallback` to skip native browser voices entirely and always use server-side HTTP audio. This provides consistent voice quality across all browsers: ```typescript const rv = await getResponsiveVoice({ apiKey: 'YOUR_KEY', forceFallback: true, }); // Toggle at runtime rv.setForceFallback(false); ``` ## Resolution Precedence When `speak()` is called, voice selection resolves in this order (highest priority first): 1. **`params.voice` override** — direct `SpeechSynthesisVoice` object; bypasses everything below 2. **[`resolveVoice` hook](/guides/voice-resolver/)** — intercepts and transforms the selector before resolution 3. **Explicit `VoiceSelector`** — the `string`, `RegExp` (or its `{ regex, flags? }` wire form), or `VoiceQuery` passed to `speak()` 4. **`defaultVoice` config** — used when no selector is provided ## Voice Matching Chain Once a voice name is determined, ResponsiveVoice walks the voice’s internal chain of system voice IDs and tries these matching strategies **in order across all chain entries** (strategy-first): 1. **Exact match** — direct name comparison against browser voices 2. **Whitespace normalized** — handles Chrome’s Unicode non-breaking spaces (U+00A0) in Asian voice names 3. **Parenthetical stripped** — handles Apple Safari’s “(Enhanced)” / “(Premium)” suffixes 4. **Partial match** — case-insensitive substring match 5. **Language fallback** — any browser voice matching the target language 6. **HTTP fallback** — server-side TTS via the ResponsiveVoice API Tip Strategy-first iteration means an exact match later in the chain always beats a partial match earlier in the chain. This ensures the most specific match wins. ## Listing Voices Use `getVoices()` to list all available voices on the current platform: ```typescript const voices = rv.getVoices(); console.log(voices); // [ // { name: 'UK English Female', lang: 'en-GB', gender: 'f' }, // { name: 'UK English Male', lang: 'en-GB', gender: 'm' }, // { name: 'US English Female', lang: 'en-US', gender: 'f' }, // ... // ] ``` ## Platform-Specific Voices Some voices are only available on specific platforms: | Platform | Notes | | -------- | ------------------------------------ | | Chrome | Google voices, best selection | | Safari | Apple voices, “(Enhanced)” variants | | Firefox | Limited native voices, uses fallback | | iOS | Version-specific voice sets | | Android | Device-dependent voices | ## Language Codes Voices use BCP-47 language codes: | Code | Language | | ------- | -------------------- | | `en-GB` | British English | | `en-US` | American English | | `fr-FR` | French | | `de-DE` | German | | `es-ES` | Spanish | | `ja-JP` | Japanese | | `zh-CN` | Chinese (Simplified) | ## Example: Voice Selector UI Try all three selector forms side by side in the [**live Voice Selector demo**](https://examples.responsivevoice.org/browser/voice-selector/) — type a name, type a regex, or build a query and watch the snippet rewrite as you switch tabs. The accompanying [docs page](/examples/voice-selector) explains the design choices. The minimal pattern below shows the simplest case (build a dropdown from `getVoices()`): ```typescript // Build a voice selector dropdown const select = document.createElement('select'); rv.getVoices().forEach((voice) => { const option = document.createElement('option'); option.value = voice.name; option.textContent = `${voice.name} (${voice.lang})`; select.appendChild(option); }); select.addEventListener('change', () => { rv.speak('Sample text', select.value); }); ``` # Web Player > Configure the web player theme, layout, controls, behavior, and advanced narration options. The **web player** is a drop-in article reader with paragraph highlighting, click-to-jump, playback controls, and a floating mini-player that follows the reader once the main controls scroll out of view. You can add a default web player to any page **without code** from the [ResponsiveVoice Dashboard](/guides/app-features/#web-player). This guide covers the **code path** — installing, configuring, and overriding the player programmatically with `init()` and `mount()`. For live examples, see the [Web Player example](/examples/web-player/) and [Web Player Customization](/examples/web-player-customization/) pages. ## At a Glance 1. Drop the CDN script in your page and call: ```javascript responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true }, }, }); ``` 2. Choose a **Theme**: `neutral`, `responsivevoice`, or custom color tokens. 3. Set the **Layout** if the player should appear somewhere other than before the article. 4. Choose **Controls** such as progress, time, skip, speed, brand, and mini-player settings. 5. Tune **Behavior** such as paragraph highlighting, click-to-jump, and skipping non-readable content. 6. Use **Advanced** only when you need custom selectors, voice override, or extra narration exclusions. ## Add the Script and Initialize Drop the CDN bundle into your page and turn the player on. By default it attaches to the first `
` element it finds and narrates every `p`, `h2`, `h3`, and `li` inside it. ```html ``` If your page has an `
`, the player appears above it after initialization. Note `init()` is async because it fetches your website configuration. The player mounts once initialization resolves; you do not need to `await` anything for a static `
` already in the DOM. ## Theme optional Theme controls the visual style of the player. Two presets ship with the player, plus a custom-token path for brand colors. Tabs preserve your selection across pages. * Neutral The default theme is light, minimal, and designed to sit comfortably on most host pages. No extra configuration is needed: ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true }, }, }); ``` * ResponsiveVoice The ResponsiveVoice theme uses the same controls and layout, recolored around the ResponsiveVoice violet. ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, theme: 'responsivevoice' }, }, }); ``` * Custom tokens Override individual color tokens to match your brand. Every field is optional; omitted values fall back to the neutral defaults. ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, theme: { fill: '#10b981', track: '#d1fae5', accentSoft: '#d1fae5', }, }, }, }); ``` The full nine-token palette is `bg`, `fg`, `muted`, `accent`, `accentSoft`, `hover`, `border`, `track`, and `fill`. The [customization picker](https://examples.responsivevoice.org/browser/web-player-customization/) lets you tune those values and copy the matching configuration. ## Layout optional Layout controls where the player appears and how much space it uses. | Config option | Default | What it does | | ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------- | | `position` | `'before'` | Keyword (`before` / `after` / `inline`) relative to `selector`, or `{ target, at }` for a custom mount target. | | `layout.mode` | `'shrink'` | `shrink` hugs the controls; `fill` spans the container. | | `layout.display` | `'block'` | `block` gives the player its own line; `inline` flows with surrounding content. | ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, position: 'before', layout: { mode: 'shrink', display: 'block', }, }, }, }); ``` For a custom mount target, pass an object: ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, position: { target: '#article-player', at: 'inside', }, }, }, }); ``` ## Controls optional Controls decide which buttons are visible and whether the floating mini-player is enabled. | Config option | Default | What it does | | ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------- | | `controls.progress` | `true` | Shows progress through the readable content. | | `controls.time` | `true` | Shows elapsed and total estimated time. | | `controls.skip` | `true` | Shows previous and next paragraph buttons. | | `controls.speed` | `true` | Shows the speed cycle button. | | `controls.brand` | `true` | Shows the ResponsiveVoice brand icon and the “Powered by” line; use `{ icon, poweredBy }` to toggle each. | | `miniPlayer.enabled` | `true` | Shows the mini-player when the main player is out of view. | | `miniPlayer.position` | `'bottom-left'` | Places the mini-player in a viewport corner or custom offset. | | `miniPlayer.animation` | `'slide'` | Uses `none`, `fade`, `slide`, or `pop`. | ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, controls: { progress: true, time: true, skip: true, speed: true, brand: true, }, miniPlayer: { enabled: true, position: 'bottom-left', animation: 'slide', }, }, }, }); ``` ## Behavior optional Behavior covers highlighting, click-to-jump, and skipping non-readable content. | Config option | Default | What it does | | ------------------------------- | ------- | ------------------------------------------------------------------------------------------- | | `navigation.paragraphHighlight` | `true` | Highlights the currently spoken element. | | `navigation.paragraphClick` | `true` | Lets visitors click a paragraph to start reading there. | | `sanitize.enabled` | `true` | Excludes scripts, styles, form controls, embedded media, and hidden content from narration. | ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, navigation: { paragraphHighlight: true, paragraphClick: true, }, sanitize: { enabled: true }, }, }, }); ``` Keep `sanitize.enabled` on for normal sites. Turning it off narrates raw matched text and is mainly useful for debugging unusual markup. ## Advanced optional Advanced options are for site-specific markup and voice overrides. Most websites can leave these at their defaults. Use them when the player needs to read a specific part of a page, skip extra elements, or use a different voice from the website default. | Config option | Default | What it does | | ------------------- | ----------------- | ------------------------------------------------------------------- | | `selector` | `'article'` | CSS selector for the element that contains readable content. | | `paragraphSelector` | `'p, h2, h3, li'` | CSS selector for readable elements inside the container. | | `voice` | Website default | Optional voice selector for this player only. | | `sanitize.exclude` | `[]` | Extra CSS selectors to skip in addition to the built-in exclusions. | ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, selector: '.post-body', paragraphSelector: 'p, h2, h3, blockquote', voice: 'UK English Female', sanitize: { enabled: true, exclude: ['.pull-quote', '.byline'], }, }, }, }); ``` If `selector` matches more than one element on the page, every match gets its own independent player. Starting playback on one automatically pauses every other player. Nested matches, such as an `
` inside another `
`, are filtered to the outermost element so duplicate players are not created. ### Place the Player in a specific container Many WordPress themes and marketing sites wrap the entire page in an `
`. In that layout, the default `selector: 'article'` can place the player above the page title and narrate calls to action or other content that is not part of the listening experience. Use a purpose-built content container and player slot instead: ```html

A better way to experience the web

Visitors can listen while they commute, work, or rest their eyes.

The current passage is highlighted as the article is read.

``` ```js window.responsiveVoice.webPlayer?.mount('#article-preview', { voice: 'UK English Female', theme: 'responsivevoice', paragraphSelector: 'h2, p', position: { target: '#article-player', at: 'inside', }, }); ``` This pattern is useful for product pages too: place a short, representative article demo after trust proof such as customer logos, then let visitors try the same player their readers would receive. ### Voice Override Without configuration, the player narrates with the website default voice. To override per player, add a `voice` field. It accepts a name string, a `RegExp`, or a structured query, exactly like the second argument to `speak()`. ```js window.responsiveVoice.init({ apiKey: 'YOUR_API_KEY', features: { webPlayer: { enabled: true, voice: 'UK English Female', rate: 0.95, }, }, }); ``` The full grammar lives in [Voice Selection](/guides/voice-selection/). The same `voice`, `rate`, `pitch`, and `volume` fields are also valid as per-mount overrides on `mount()`. ### Exclude From Narration Mark any element with `data-rv-skip` to keep it out of the narration. The player still highlights surrounding paragraphs; the skipped element is invisible to the reader’s progression. ```html

Read this paragraph.

Continue from here.

``` Useful for code blocks, figure captions, ad slots, pull quotes, and bylines. By default the player never reads content that is not visible prose. Scripts, styles, form controls, embedded media, and elements marked `hidden` or `aria-hidden="true"` are excluded from narration even when they sit inside a matched paragraph. A paragraph left empty after sanitization is dropped from the reading flow. Add your own selectors with `sanitize.exclude`. Those selectors are added to the built-in exclusions; they do not replace the built-in list. ## Testing After integrating, verify on the real page — its markup determines what’s found and narrated: * Press play and confirm the selected voice sounds right. * Confirm the configured controls (progress, time, skip, speed, brand) appear. * Click a paragraph if `navigation.paragraphClick` is enabled. * Confirm highlighting appears if `navigation.paragraphHighlight` is enabled. * Scroll until the main player leaves view and confirm the mini-player appears if enabled. * Add a known excluded element and confirm it is skipped. ## Dynamic Content For SPAs and lazy-loaded sections, the article may not be in the DOM when `init()` runs, so the auto-discovery pass misses it. Use `webPlayer.mount()` to attach a player after the element is rendered: ```js const html = await fetch('/posts/123').then((r) => r.text()); document.getElementById('post-area').innerHTML = html; const handle = window.responsiveVoice.webPlayer?.mount('#post-area'); // later, before removing the element: handle?.unmount(); ``` `mount()` accepts the same option shape as the init config, leaf-merged over it. For example, `mount('#post-area', { rate: 1.2 })` keeps every other setting intact and only changes the rate. It returns `null` if the feature is not enabled or the element cannot be found, so the `?.` guards are intentional. Caution Always call `handle.unmount()` before you remove the host element from the DOM. The player keeps DOM listeners attached to the article and the document. A forgotten unmount will not crash anything, but it can leak event handlers across SPA navigations. ## Troubleshooting ### The player doesn’t appear * Confirm `init()` ran and resolved, and `features.webPlayer.enabled` is `true`. * Confirm `selector` matches an element that is already in the DOM when `init()` runs. Content rendered later (SPA, lazy-load) is missed by the one-time auto-discovery pass — mount it explicitly with [`mount()`](#dynamic-content). ### `mount()` returned `null` It returns `null` when the feature isn’t enabled or the target element isn’t in the DOM — which is why the `?.` guards matter. Confirm `enabled: true` and that the element exists before the call. ### The wrong content is read, or paragraphs are missing * `paragraphSelector` must match the elements you want narrated (default `p, h2, h3, li`). * Sanitization drops scripts, styles, form controls, media, and `hidden` / `aria-hidden` nodes, plus anything matching `data-rv-skip` or your `sanitize.exclude` selectors; a paragraph left empty after sanitization is skipped. ### The mini-player doesn’t appear `miniPlayer.enabled` must be `true`, and it only surfaces while playback is active and the main player has scrolled out of view. ### Handlers leak across SPA navigations Call `handle.unmount()` before removing the host element — the player keeps listeners on the article and document until you do. ## Next Steps [Live demo](https://examples.responsivevoice.org/browser/web-player/)The basic integration running in your browser. Source on GitHub, ready to copy and adapt. [Customize live](https://examples.responsivevoice.org/browser/web-player-customization/)Tweak every option in real time. The matching init() and mount() snippets update alongside the player. [Web Player example walkthrough](/examples/web-player/)In-docs walkthrough of multi-mount, imperative mount, and skipped content. [Voice Selection](/guides/voice-selection/)The selector grammar used by the voice field: name string, regex, or structured query. # Why ResponsiveVoice? > How ResponsiveVoice v2 compares to ElevenLabs, Microsoft Azure, and Google Cloud for adding text-to-speech to a website. ResponsiveVoice is an open-source, TypeScript-first text-to-speech layer for the web. It is the integration and control layer between your site and one or more voice engines — browser-native, ResponsiveVoice’s hosted voices, or premium providers you bring yourself. Note ResponsiveVoice v2 is a complete rewrite of the original library — MIT-licensed, fully typed, and split into focused packages. Older reviews describing it as a fixed-price Web Speech API wrapper predate this release. ## What ResponsiveVoice gives you * **Open source, MIT-licensed.** The client (`@responsivevoice/core`) is a drop-in replacement for the legacy `responsivevoice.js` — same `speak`/`cancel`/`pause`/`resume` API, now typed and tree-shakeable. * **Browser-native with automatic fallback.** Uses the Web Speech API when the device has a matching voice, and falls back to hosted server voices when it doesn’t — one API, consistent behavior across browsers. * **100+ voices across 50+ languages**, fetched and cached at runtime, so the catalog improves without a package upgrade. * **Bring Your Own Key (BYOK).** Route premium voices from **Google Cloud**, **Microsoft Azure**, and **OpenAI** through ResponsiveVoice using your own provider key — you keep the provider relationship and the per-character billing, and gain RV’s browser integration, fallback, streaming, and player features on top. * **Streaming playback.** HTTP audio or WebSocket, so speech starts before the full clip is ready. * **Predictable pricing.** A free plan plus fixed-tier plans, instead of metering every character. * **REST + WebSocket API** documented by an OpenAPI 3.1 specification, for server-side and non-browser use. ## How it compares The premium providers below produce excellent neural audio. The distinction is that ResponsiveVoice is the **integration layer** — and with BYOK it can front those same providers rather than competing with them. | Capability | ResponsiveVoice | ElevenLabs | Azure / Google Cloud | | ------------------------- | -------------------------------- | ---------------------------- | -------------------- | | Open source (MIT) | Yes | No | No | | Drop-in browser script | Yes | Via your own backend | Via your own backend | | Browser-native + fallback | Yes | Cloud only | Cloud only | | Premium neural voices | Via BYOK (Azure, OpenAI, Google) | Native | Native | | Voice cloning | No | Yes | Limited | | Pricing model | Free plan + fixed tiers | Per-character / subscription | Per-character | | Streaming | HTTP + WebSocket | Yes | Yes | ## When to choose what * **Choose ResponsiveVoice** when you want a drop-in, open-source browser TTS with native-plus-fallback behavior, predictable pricing, and the option to bring premium provider voices via BYOK without re-architecting — read-aloud, accessibility, language learning, article narration, and announcements. * **Choose a provider directly** when best-in-class voice cloning or maximum expressive realism is the product itself, and you’re set up to manage the integration and per-character billing. Many sites use both: ResponsiveVoice for the browser integration and player, with a premium provider supplied via BYOK for the voices. ## Next steps * [Installation](/getting-started/installation/) — add the script or install from npm. * [Voice Selection](/guides/voice-selection/) — filter and resolve voices. * [REST API](/rest-api/) — server-side synthesis.