This is the abridged developer documentation for ResponsiveVoice # ResponsiveVoice > The modern, TypeScript-first text-to-speech library for browsers and Node.js ## Packages [Section titled “Packages”](#packages) @responsivevoice/core The heart -- and lungs -- of ResponsiveVoice. A drop-in replacement for the former **responsivevoice.js**, with modern TypeScript support, that runs in the browser. @responsivevoice/api-client REST API client with retry logic, response validation, and beautiful error handling. @responsivevoice/types Shared Schemas and Types used in the entire ecosystem. ## Features [Section titled “Features”](#features) TypeScript First Full TypeScript support with strict types and IntelliSense. Modern Build ESM, CJS, and a browser bundle (CDN). Tree-shakeable exports. 100+ Voices Access to 100+ voices across 50+ languages. Backward Compatible Drop-in replacement for legacy responsivevoice.js. *For LLMs and AI agents: [llms.txt](/llms.txt), [llms-full.txt](/llms-full.txt), or the [For AI Agents](/guides/ai-agents/) guide — append `.md` to any docs URL.* # Examples > Learn by example with working demos and code samples Explore working examples that demonstrate ResponsiveVoice capabilities. All examples live in the [examples repository](https://github.com/responsivevoice/examples). ## CDN Examples [Section titled “CDN Examples”](#cdn-examples) Drop a ` ``` Without an API key the library runs in **demo mode** — it falls back to browser-native Web Speech API voices. Call `rv.isDemoMode()` at runtime to detect this state (for example, to surface a "Demo Mode" indicator in your UI). ## Next Steps [Section titled “Next Steps”](#next-steps) * [Extended Usage Example](/examples/extended) — voice selection, speech controls, event logging * [npm + Vite Example](/examples/vite) — the same flow via ESM import and a bundler * [Events Guide](/guides/events) — the full event lifecycle * [Voice Selection Guide](/guides/voice-selection) — picking voices programmatically # CLI Tool > Command-line text-to-speech utility for Node.js A command-line tool that synthesizes text to speech and saves audio to a file. Uses `@responsivevoice/api-client` directly — server-side only, no browser involvement. ## Quick start [Section titled “Quick start”](#quick-start) ```bash export RESPONSIVEVOICE_API_KEY="your-api-key" # https://app.responsivevoice.org export RESPONSIVEVOICE_API_SECRET="your-api-secret" # "Server-to-server API secrets" npm install npm run cli -- "Hello, world!" --output hello.mp3 ``` Unlike the browser examples, the CLI has no demo mode — the API client requires a valid key and secret to make HTTP requests to the synthesis endpoint. The `README` in the source tree lists every flag (`--voice`, `--rate`, `--pitch`, `--volume`, `--stdout`, `--list-voices`). ## Next Steps [Section titled “Next Steps”](#next-steps) * [HTTP Server](/examples/server) — REST wrapper around the same API client * [REST API Overview](/rest-api/overview/) — direct HTTP usage * [API Client Reference](/api/api-client/src) — full client documentation # Events & Callbacks Example > Live view of every public event and per-call callback firing A focused teacher for the speech lifecycle — speak one utterance and watch every public event and per-call callback fire in real time. ## Why a separate example [Section titled “Why a separate example”](#why-a-separate-example) There are two parallel ways to observe speech: `responsiveVoice.addEventListener(name, fn)` (process-wide, persists across calls) and inline `onstart` / `onend` / `onerror` / `onboundary` callbacks passed to `speak()` (bound to a single utterance). Most confusion in the wild stems from mixing them up. This example surfaces both at the same visual level — a row of per-call pills bound to one utterance, and a row of global pills that fire across the lifetime of the page — so the distinction is concrete instead of conceptual. ## What you'll see [Section titled “What you'll see”](#what-youll-see) * **Two rows of pills.** The top row (four pills: `onstart`, `onboundary`, `onend`, `onerror`) are the per-call callbacks you pass to `speak()`. The bottom row (nine pills: `OnReady`, `OnVoiceResolved`, `OnStart`, `OnPartStart`, `OnPartEnd`, `OnPause`, `OnResume`, `OnEnd`, `OnError`) are the global events you register with `addEventListener()`. * **The default text triggers chunking.** The text box starts with a multi-sentence sample long enough that the engine splits it into several parts. Click Speak and you'll see `OnPartStart` show `part X of N` and `OnPartEnd` count up as each chunk finishes. * **Pills reset on every Speak click**, except `OnReady` — that one fires once when the page loads and stays green for the rest of the session. * **The full event log sits below the pills.** Once you recognize a pill, you can scroll the log to see the actual data each event carries. * **`onboundary` only fires for browser-native voices.** When a voice plays through the HTTP fallback (server-side audio), the boundary callback stays quiet and its counter doesn't move. * **`OnPause` has a \~60-second browser limit.** If you click Pause and don't click Resume within about a minute, the browser cancels the speech automatically and you'll see `OnEnd` fire instead of `OnResume`. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Events Guide](/guides/events) — full event reference and async/await patterns * [Basic Example](/examples/basic) — minimal `OnReady` + `speak()` flow without the inspection scaffolding # Extended Usage Example > Full-featured demo with voice selection, speech controls, and event logging A full-featured example showing voice selection, playback controls, speech parameter tuning, and a live event log. ## What it covers [Section titled “What it covers”](#what-it-covers) * Voice browser with language filtering * `speak` / `pause` / `resume` / `cancel` controls * Rate / pitch / volume sliders * Platform detection (browser, OS, device type) * Real-time event log * Demo mode indicator driven by `rv.isDemoMode()` * Force-fallback toggle (use HTTP audio instead of Web Speech API) ## Speech parameter ranges [Section titled “Speech parameter ranges”](#speech-parameter-ranges) | Parameter | Min | Default | Max | Description | | --------- | --- | ------- | --- | ------------ | | `rate` | 0.1 | 1.0 | 2.0 | Speech speed | | `pitch` | 0.1 | 1.0 | 2.0 | Voice pitch | | `volume` | 0.0 | 1.0 | 1.0 | Audio volume | ```js rv.speak('Hello!', 'UK English Female', { rate: 1.2, pitch: 1.0, volume: 0.8, }); ``` ## Force fallback mode [Section titled “Force fallback mode”](#force-fallback-mode) Bypass the Web Speech API and always use HTTP audio from the server: ```js await rv.init({ apiKey: 'your-api-key', forceFallback: true }); ``` Useful when you need consistent audio across browsers, or when Web Speech API voices are unavailable on the target device. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Basic Example](/examples/basic) — the minimal starting point * [Voice Selection Guide](/guides/voice-selection) — advanced voice filtering * [Events Guide](/guides/events) — full event reference # HTTP Server > REST API server for text-to-speech synthesis A minimal HTTP server that exposes REST endpoints for text-to-speech synthesis. Useful as a reverse proxy — keeps your API key off the browser and lets your frontend call your own origin. ## Quick start [Section titled “Quick start”](#quick-start) ```bash export RESPONSIVEVOICE_API_KEY="your-api-key" # https://app.responsivevoice.org export RESPONSIVEVOICE_API_SECRET="your-api-secret" # "Server-to-server API secrets" npm install npm run server # Server on http://localhost:3001 ``` ## Endpoints [Section titled “Endpoints”](#endpoints) | Method | Path | Description | | ------ | ---------------------- | ----------------------------- | | GET | `/` | API documentation | | GET | `/voices` | List all voices | | GET | `/voices/:lang` | Voices by language | | POST | `/synthesize` | Synthesize speech (JSON body) | | GET | `/synthesize?text=...` | Synthesize via query params | ## Calling from a frontend [Section titled “Calling from a frontend”](#calling-from-a-frontend) ```js async function speak(text) { const response = await fetch('http://localhost:3001/synthesize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, voice: 'UK English Female' }), }); const audio = new Audio(URL.createObjectURL(await response.blob())); audio.play(); } ``` The server ships with permissive CORS headers (`Access-Control-Allow-Origin: *`). Tighten to your domain(s) before deploying anything public-facing. ## Next Steps [Section titled “Next Steps”](#next-steps) * [CLI Tool](/examples/cli) — same API client, command-line form * [REST API Overview](/rest-api/overview/) — direct HTTP usage * [API Client Reference](/api/api-client/src) — full client documentation # npm + Vite Example > Installed from npm, bundled for the browser by Vite `@responsivevoice/core` installed from npm and bundled for the browser by [Vite](https://vitejs.dev/). Same feature set as the [Basic Example](/examples/basic), but the library arrives via ESM `import` instead of a CDN ` ``` The feature ships in the CDN bundle — no extra imports. ## Multi-mount [Section titled “Multi-mount”](#multi-mount) When `selector` matches more than one top-level element, an independent player is mounted on each. Single-active-narrator coordination is implicit: starting playback on one resets every other player to idle. Nested matches (e.g. `
` inside `
`) are filtered automatically — only the outermost match mounts. Publishers who specifically want nested mounts write a more specific selector. ## Imperative mount [Section titled “Imperative mount”](#imperative-mount) For SPAs and lazy-loaded sections, call `rv.webPlayer?.mount(selectorOrElement, overrides?)` after the element is in the DOM: ```js fetch('/article/123') .then((r) => r.text()) .then((html) => { document.getElementById('dynamic-area').innerHTML = html; const handle = rv.webPlayer?.mount('#dynamic-area', { theme: 'neutral' }); // later, before removing the element from the DOM: // handle?.unmount(); }); ``` Overrides are leaf-merged over the init config — `{ controls: { brand: false } }` keeps the rest of the init defaults intact. ## Skipping content [Section titled “Skipping content”](#skipping-content) Any element carrying `data-rv-skip` is excluded from narration: ```html

Read this paragraph.

This block is skipped.

Continue reading from here.

``` ## Next steps [Section titled “Next steps”](#next-steps) * [Web Player Customization](/examples/web-player-customization) — live control panel for every option. # Web Player Customization > Live control panel for every webPlayer option — theme, controls, navigation, layout, and CSS spacing variable. Tune every `webPlayer` option in real time. Each toggle re-mounts the player via `rv.webPlayer.mount()` with leaf-merged overrides, and the matching JSON config snippet updates alongside the player so you can copy-paste the result into your own setup. ## What you can tweak [Section titled “What you can tweak”](#what-you-can-tweak) * **Theme** — `Neutral`, `ResponsiveVoice`, or custom tokens (9-token color picker covering bg, fg, muted, accent, accentSoft, hover, border, track, fill). * **Controls** — toggle progress, time, skip, speed, brand individually. Play / pause is always shown. * **Navigation** — paragraph highlight and click-to-jump independently. * **Position** — main-pill placement: keyword (`before` / `after` / `inline`) relative to the container, or `{ target, at }` for a custom mount target. * **Layout** — main-pill width (`shrink` / `fill`) and outer display (`block` / `inline`). * **Mini-Player** — visibility, viewport corner (or CSS-offset object), and entrance/exit animation. * **Sanitize** — keep scripts, styles, controls, and media out of narration (on by default), plus extra `exclude` selectors of your own. * **Spacing** — the `--rv-player-margin` CSS custom property, controlling the player's surrounding margin. ## How re-mount works [Section titled “How re-mount works”](#how-re-mount-works) The example panel keeps a single mount handle and replaces it on every change: ```js let handle = null; function remount() { handle?.unmount(); handle = rv.webPlayer?.mount('#sample', buildConfig()); } ``` `buildConfig()` reads the panel state and returns a `WebPlayerMountOverrides` object that's leaf-merged over the init config. The same shape works in `rv.init({ features: { webPlayer: ... } })` for static sites. ## Custom mount target [Section titled “Custom mount target”](#custom-mount-target) The `position` field accepts either a keyword (`'before'`, `'after'`, `'inline'`) relative to `selector`, or an object that mounts the player into any element on the page. This is useful when the article element is constrained by your layout (sidebar, fixed slot, CMS-driven composition): ```ts rv.webPlayer?.mount('#article', { position: { target: '#player-slot', at: 'inside' }, }); ``` `at` accepts `'inside'` (first child of the target, the default), `'before'` (sibling before), or `'after'` (sibling after). `target` is required — the keyword form covers the article-relative case. If `target` doesn't match anything in the DOM at mount time, the player falls back to the keyword `'before'` and logs a warning. ## Mini-player position [Section titled “Mini-player position”](#mini-player-position) The floating mini-player surfaces when the main player scrolls out of view. Its viewport corner is configurable via `miniPlayer.position`: ```ts rv.webPlayer?.mount('#article', { miniPlayer: { enabled: true, position: 'bottom-right' }, }); ``` Accepted values: * A corner keyword: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`. Default `'bottom-left'`. * A CSS-offset object: `{ top, right, bottom, left }`, each a CSS length string. At least one side is required; opposing sides (`top` + `bottom`, `left` + `right`) are rejected. ```ts rv.webPlayer?.mount('#article', { miniPlayer: { enabled: true, position: { top: '80px', right: '20px' } }, }); ``` The boolean shorthand is the natural form for the on/off case — `miniPlayer: true` enables the mini-player at the default corner, `miniPlayer: false` disables it. ## Mini-player animation [Section titled “Mini-player animation”](#mini-player-animation) `miniPlayer.animation` controls how the mini-player enters and leaves as you scroll: ```ts rv.webPlayer?.mount('#article', { miniPlayer: { enabled: true, animation: 'fade' }, }); ``` Accepted values: * `'slide'` — fades in and slides from the docked corner. Default. * `'fade'` — fades in and out, no movement. * `'pop'` — fades in with a brief scale-up. * `'none'` — appears and disappears instantly. The slide direction follows where the mini-player sits: anything in the lower half of the viewport rises into place, anything in the upper half drops in — so corner keywords and custom offsets both enter from the nearest edge. Motion is skipped automatically when the browser requests reduced motion, so every preset falls back to an instant swap for those readers. ## Choosing a voice per player [Section titled “Choosing a voice per player”](#choosing-a-voice-per-player) By default every web player narrates with the website's default voice (the `voice` profile from `/v2/config`). The web-player config exposes four flat playback fields — `voice`, `rate`, `pitch`, `volume` — that mirror the arguments of `core.speak(text, voice, params)`. Each is independently optional and overrides the website default for one player: ```ts rv.webPlayer?.mount('#article', { voice: 'US English Male', rate: 0.9, }); ``` `voice` accepts the full [`VoiceSelector`](/guides/voice-selection/) grammar. In JS, write whichever form is most natural — strings for named voices, real `RegExp` literals for patterns, plain objects for structured queries. The schema normalizes a `RegExp` to its JSON-clean `{ regex, flags }` form on parse, so the same selector works identically in JS, server config, and every SDK language: ```ts // 1. Exact voice name rv.webPlayer?.mount('#en-article', { voice: 'UK English Female', }); // 2. Structured query — pick a Portuguese female voice from any provider rv.webPlayer?.mount('#pt-article', { voice: { lang: 'pt', gender: 'female' }, }); // 3. Regex pattern — first voice whose name matches rv.webPlayer?.mount('#multi-article', { voice: /English.*Male/i, }); ``` The shape parallels `speak()` directly: ```ts // Direct speak() call: rv.speak('Hello', /UK English/, { rate: 0.9, volume: 0.8 }); // ^text ^voice ^speech params // Equivalent web-player config — same fields, same names: rv.webPlayer?.mount('#article', { voice: /UK English/, rate: 0.9, volume: 0.8, }); ``` ### Setting a default for every player [Section titled “Setting a default for every player”](#setting-a-default-for-every-player) The same fields are valid in the website config (`webPlayer.voice` / `rate` / `pitch` / `volume`), in which case every auto-discovered article starts with those values instead of the website-wide default: ```ts rv.init({ features: { webPlayer: { enabled: true, voice: 'US English Male', rate: 0.95, }, }, }); ``` Per-mount overrides leaf-merge over this config, so a player that mounts with `{ rate: 1.2 }` keeps the `'US English Male'` voice and only changes the rate. ### What inherits, what overrides [Section titled “What inherits, what overrides”](#what-inherits-what-overrides) Each playback field is independently optional. Any field you omit falls through to the website default voice profile (the profile's `name` becomes the string-form `voice` selector when none is set): | Field | When omitted | | -------- | --------------------------------------------------------- | | `voice` | Inherits the website default voice's `name` as a selector | | `pitch` | Inherits the website default `pitch` | | `rate` | Inherits the website default `rate` | | `volume` | Inherits the website default `volume` | ## Next steps [Section titled “Next steps”](#next-steps) * [Web Player Example](/examples/web-player) — the basic integration without the customization panel. * [Voice Selection](/guides/voice-selection/) — the full `VoiceSelector` grammar used by the `voice` field. # Installation > How to install ResponsiveVoice packages ## Core package [Section titled “Core package”](#core-package) The main client library for browser text-to-speech. Both paths deliver the same package — pick the one that fits your project: * **Browser bundle (CDN)** — no build step; add one script tag and use the global `responsiveVoice`. Best for plain HTML, prototypes, CMS sites, and legacy pages. Call `init()` with your API key — without it you get demo mode (browser default voice only). * **npm** — for apps with a bundler (Vite, webpack, Next). Install and import. - Browser bundle (CDN) ```html ``` - npm ```bash npm install @responsivevoice/core # or: pnpm add @responsivevoice/core # or: yarn add @responsivevoice/core ``` ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; const rv = await getResponsiveVoice({ apiKey: 'YOUR_API_KEY' }); ``` ## API client [Section titled “API client”](#api-client) REST API client for direct server communication. * npm ```bash npm install @responsivevoice/api-client ``` * pnpm ```bash pnpm add @responsivevoice/api-client ``` * yarn ```bash yarn add @responsivevoice/api-client ``` ## Types package [Section titled “Types package”](#types-package) `@responsivevoice/types` (TypeScript types + Zod schemas) ships automatically as a dependency of the packages above, and `api-client` re-exports the common ones — so you rarely install it directly. If you want the schemas standalone: `npm install @responsivevoice/types`. ## Requirements [Section titled “Requirements”](#requirements) ### Browsers [Section titled “Browsers”](#browsers) Chrome 66+, Firefox 57+, Safari 12+, Edge 17+, iOS Safari 12+, Chrome Android 66+. The browser bundle (CDN) includes all necessary polyfills and works in these browsers without any additional setup. ESM/CJS packages are syntax-compatible but leave browser polyfilling to your bundler. ### Node.js [Section titled “Node.js”](#nodejs) | Package | Minimum | Full functionality | | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@responsivevoice/types` | 14+ | All features | | `@responsivevoice/text` | 14+ | All features | | `@responsivevoice/api-client` | 16+ | HTTP synthesis requires Node 18+ for native `fetch` and `Blob`. On Node 16–17, pass a `fetch` implementation via the `fetch` config option (e.g. `node-fetch` or `undici`). | | `@responsivevoice/core` | 16+ | Browser-only (Web Speech API + audio playback); Node is the build environment (bundle with Vite/webpack), not a runtime target | | `@responsivevoice/features` | 14+ | Browser-only (DOM APIs) | WebSocket streaming requires Node 22+ for the native `WebSocket` global, or pass any W3C-compatible WebSocket implementation (e.g. the `ws` package) via the `WebSocket` config option. ### TypeScript [Section titled “TypeScript”](#typescript) 5.0+ (optional, but recommended). Full type definitions included in all packages. # Migration Guide > Upgrade from legacy responsivevoice.js to @responsivevoice/core This guide helps you migrate from the legacy `responsivevoice.js` script to the modern `@responsivevoice/core` package. ## Overview [Section titled “Overview”](#overview) The new package provides: * **TypeScript support** with full type definitions * **Modern build formats** (ESM, CJS, browser bundle) * **Tree-shakeable exports** * **Better error handling** * **API-first architecture** (voice data fetched at runtime) * **Full backward compatibility** with the legacy API ## Installation Changes [Section titled “Installation Changes”](#installation-changes) ### Before [Section titled “Before”](#before) ```html ``` ### After [Section titled “After”](#after) The closest migration keeps a script tag — load the browser bundle from the CDN and move your key into an `init()` call. Use npm instead if your app has a bundler. * Browser bundle (CDN) ```html ``` * npm ```bash npm install @responsivevoice/core ``` ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; const rv = await getResponsiveVoice({ apiKey: 'YOUR_KEY' }); ``` ## API Mapping [Section titled “API Mapping”](#api-mapping) The core API remains the same: | Legacy | Modern | Notes | | ----------------------------- | ---------------- | -------------- | | `responsiveVoice.speak()` | `rv.speak()` | Same signature | | `responsiveVoice.cancel()` | `rv.cancel()` | Same behavior | | `responsiveVoice.pause()` | `rv.pause()` | Same behavior | | `responsiveVoice.resume()` | `rv.resume()` | Same behavior | | `responsiveVoice.getVoices()` | `rv.getVoices()` | Same behavior | | `responsiveVoice.isPlaying()` | `rv.isPlaying()` | Same behavior | In the browser bundle the global stays `responsiveVoice`; with npm you call the same methods on the `rv` instance from `getResponsiveVoice()`. The signatures are identical either way. ## Configuration Changes [Section titled “Configuration Changes”](#configuration-changes) ### Before [Section titled “Before”](#before-1) ```javascript // Global configuration via URL parameter ; // Or via global variable window.responsiveVoice.setDefaultVoice('UK English Female'); ``` ### After [Section titled “After”](#after-1) * Browser bundle (CDN) ```html ``` * npm ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; const rv = await getResponsiveVoice({ apiKey: 'YOUR_KEY', defaultVoice: 'UK English Female', }); ``` ## Event System [Section titled “Event System”](#event-system) Per-call event callbacks are unchanged from the legacy script: * Browser bundle (CDN) ```javascript responsiveVoice.speak('Hello', 'UK English Female', { onstart: () => console.log('Started'), onend: () => console.log('Ended'), }); ``` * npm ```typescript rv.speak('Hello', 'UK English Female', { onstart: () => console.log('Started'), onend: () => console.log('Ended'), }); ``` ## Breaking Changes [Section titled “Breaking Changes”](#breaking-changes) Caution The following changes may require code updates: ### 1. Voice Data is Fetched at Runtime [Section titled “1. Voice Data is Fetched at Runtime”](#1-voice-data-is-fetched-at-runtime) Voice mappings are no longer bundled in the JavaScript file — `init()` fetches them from the API and caches them locally. **Impact:** First initialization needs a network connection to fetch the voice catalog. There's nothing to pre-fetch — `init()` already loads voices before it resolves. ### 2. No Internet Explorer Support [Section titled “2. No Internet Explorer Support”](#2-no-internet-explorer-support) The browser bundle (CDN) ships the polyfills modern browsers need and targets Chrome 66+, Firefox 57+, Safari 12+, Edge 17+, and iOS 12+. Internet Explorer 11 is not supported. **Impact:** If you need to support IE11 or similarly old browsers, stay on the legacy `responsivevoice.js` (v1). ## TypeScript Support [Section titled “TypeScript Support”](#typescript-support) The new package includes full TypeScript definitions: ```typescript import { getResponsiveVoice, type ResponsiveVoiceInitOptions, } from '@responsivevoice/core'; import type { SpeakParams } from '@responsivevoice/types'; const options: ResponsiveVoiceInitOptions = { apiKey: 'YOUR_KEY', }; const rv = await getResponsiveVoice(options); const params: SpeakParams = { pitch: 1.0, rate: 1.0, onend: () => console.log('Done'), }; rv.speak('Hello', 'UK English Female', params); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ## Need Help? [Section titled “Need Help?”](#need-help) * [GitHub Issues](https://github.com/responsivevoice/core/issues) * [API Reference](/api/core/src/) * [Support](https://responsivevoice.org/support) # Quick Start > Get up and running with ResponsiveVoice in minutes ## Basic Usage [Section titled “Basic Usage”](#basic-usage) Drop in the script tag or install from npm — both reach the same API: * Browser bundle (CDN) ```html ``` * npm ```bash npm install @responsivevoice/core # or: pnpm add @responsivevoice/core / yarn add @responsivevoice/core ``` ```typescript import { getResponsiveVoice } from '@responsivevoice/core'; const rv = await getResponsiveVoice({ apiKey: 'YOUR_API_KEY' }); rv.speak('Hello, world!'); ``` ## Voice Selection [Section titled “Voice Selection”](#voice-selection) ```typescript // Get available voices const voices = rv.getVoices(); console.log(voices); // Speak with a specific voice rv.speak('Hello', 'UK English Female'); rv.speak('Hello', 'US English Male'); rv.speak('Hallo', 'Deutsch Female'); ``` ## Playback Control [Section titled “Playback Control”](#playback-control) ```typescript // Pause speech rv.pause(); // Resume speech rv.resume(); // Cancel speech rv.cancel(); // Check if speaking if (rv.isPlaying()) { console.log('Currently speaking'); } ``` ## Events [Section titled “Events”](#events) ```typescript // Per-call callbacks — bound to this speak() call rv.speak('Hello with events', 'UK English Female', { onstart: () => console.log('Speech started'), onend: () => console.log('Speech ended'), onerror: (error) => console.error('Speech error:', error), onboundary: (charIndex, name) => console.log('Word boundary:', charIndex, name), }); // Global events — fire across every speak() call (pause/resume live here) rv.addEventListener('OnPause', () => console.log('Speech paused')); rv.addEventListener('OnResume', () => console.log('Speech resumed')); ``` # 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 [Section titled “Agent Skill”](#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 [Section titled “Endpoints”](#endpoints) [/llms.txt ](/llms.txt)Index of the documentation sets, following the llmstxt.org convention. [/llms-full.txt ](/llms-full.txt)The complete documentation as a single text file. [/llms-small.txt ](/llms-small.txt)An abridged build with non-essential content removed. [/llms.md ](/llms.md)A Markdown index of every page. ## Per-page Markdown [Section titled “Per-page Markdown”](#per-page-markdown) Append `.md` to any documentation URL to get its Markdown source — for example, `/getting-started/installation.md`. ## Discovery [Section titled “Discovery”](#discovery) * `robots.txt` advertises the XML sitemap (`/sitemap-index.xml`) and permits search indexing and AI grounding. * `/llms.txt` is served at the well-known path from the [llmstxt.org](https://llmstxt.org) convention. # 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. ## Quick Reference [Section titled “Quick Reference”](#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 [Section titled “Welcome Message”](#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 [Section titled “Speak Selected Text”](#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 [Section titled “Speak Links”](#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 [Section titled “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 [Section titled “Paragraph Navigation”](#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 [Section titled “Inactivity Message”](#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 [Section titled “End-of-Page Message”](#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 [Section titled “Exit-Intent Message”](#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 [Section titled “Multiple Messages”](#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 [Section titled “Web Player”](#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 [Section titled “Theme”](#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 [Section titled “Layout”](#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* | Mounts the player before, after, inline inside, 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 [Section titled “Controls”](#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 [Section titled “Behavior”](#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 [Section titled “Advanced”](#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 [Section titled “Troubleshooting”](#troubleshooting) #### The player doesn't appear [Section titled “The player doesn't appear”](#the-player-doesnt-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 [Section titled “The player is in the wrong place”](#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 [Section titled “The wrong text is read, or paragraphs are missing”](#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 [Section titled “The mini-player doesn't appear”](#the-mini-player-doesnt-appear) Confirm **Floating mini-player** is on, then scroll until the main player leaves view while it is playing. ### Live preview and testing [Section titled “Live preview and testing”](#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 [Section titled “Advanced Usage”](#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 works across all modern browsers with automatic fallback. @responsivevoice/api-client * ![Chrome](/_astro/chrome.CRByiUFQ_ZIwTCu.svg)Chrome66+ * ![Firefox](/_astro/firefox.1bWoP6pv_ZXpkDg.svg)Firefox57+ * ![Safari](/_astro/safari.na3_-uQk_PMAUK.svg)Safari12+ * ![Edge](/_astro/edge.HDH_c98u_1zCODK.svg)Edge17+ * ![Opera](/_astro/opera.BNOhXFRG_2faPJg.svg)Opera53+ * ![iOS Safari](/_astro/safari.na3_-uQk_PMAUK.svg)iOS Safari12+ * ![Chrome Android](/_astro/chrome.CRByiUFQ_ZIwTCu.svg)Chrome Android66+ * ![Android WebView](/_astro/android-webview_64x64._FpYT5CQ_1iPoo.webp)Android WebView66+ @responsivevoice/core * ![Chrome](/_astro/chrome.CRByiUFQ_ZIwTCu.svg)Chrome66+ * ![Firefox](/_astro/firefox.1bWoP6pv_ZXpkDg.svg)Firefox57+ * ![Safari](/_astro/safari.na3_-uQk_PMAUK.svg)Safari14+ * ![Edge](/_astro/edge.HDH_c98u_1zCODK.svg)Edge16+ * ![Opera](/_astro/opera.BNOhXFRG_2faPJg.svg)Opera53+ * ![iOS Safari](/_astro/safari.na3_-uQk_PMAUK.svg)iOS Safari14+ * ![Chrome Android](/_astro/chrome.CRByiUFQ_ZIwTCu.svg)Chrome Android66+ * ![Android WebView](/_astro/android-webview_64x64._FpYT5CQ_1iPoo.webp)Android WebView66+ *Data generated from [MDN Browser Compat Data](https://github.com/mdn/browser-compat-data)* ## How Fallback Works [Section titled “How Fallback Works”](#how-fallback-works) ![Diagram](/d2/docs/guides/browser-support-0.svg) ## Native vs Fallback [Section titled “Native vs Fallback”](#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 | | | ## Platform-Specific Notes [Section titled “Platform-Specific Notes”](#platform-specific-notes) ### Chrome / Chromium [Section titled “Chrome / Chromium”](#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 [Section titled “Safari / iOS”](#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 [Section titled “Firefox”](#firefox) * Limited native voice support * Falls back to API more frequently * No boundary events when using fallback * Full functionality with API fallback ### Edge [Section titled “Edge”](#edge) * Windows voices available (David, Zira, etc.) * Good Web Speech API support * Microsoft neural voices on Windows 10+ ## Mobile Considerations [Section titled “Mobile Considerations”](#mobile-considerations) ### iOS [Section titled “iOS”](#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'); }); ``` ### Android [Section titled “Android”](#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 [Section titled “Android WebView”](#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 [Section titled “Node.js Support”](#nodejs-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) [Section titled “Node.js 18+ (recommended)”](#nodejs-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 [Section titled “Node.js 16–17”](#nodejs-1617) 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 [Section titled “WebSocket streaming on Node.js < 22”](#websocket-streaming-on-nodejs--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 [Section titled “Feature Detection”](#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 [Section titled “Forcing Fallback”](#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 [Section titled “User Interaction Requirements”](#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 [Section titled “Workaround: Initialize on First Interaction”](#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 [Section titled “Testing Across Platforms”](#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 [Section titled “Event System”](#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. ## Per-call callbacks [Section titled “Per-call callbacks”](#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}`); }, }); ``` ## Global events [Section titled “Global events”](#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 [Section titled “Removing listeners”](#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 [Section titled “Error handling”](#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 [Section titled “Async/await pattern”](#asyncawait-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 [Section titled “Progress tracking UI”](#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 [Section titled “Queue events”](#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. # Text Chunking > How ResponsiveVoice handles long text ## Overview [Section titled “Overview”](#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? [Section titled “Why Chunking?”](#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 [Section titled “Automatic Chunking”](#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 [Section titled “Chunk Boundaries”](#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) ## Default Limits [Section titled “Default Limits”](#default-limits) | Setting | Value | Description | | ------------------ | --------- | -------------------------------------- | | Default chunk size | 100 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 | ## Custom Configuration [Section titled “Custom Configuration”](#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 ``` ## Seamless Playback [Section titled “Seamless Playback”](#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 [Section titled “Manual Chunking”](#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 [Section titled “Progress with Chunks”](#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 [Section titled “Special Characters”](#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 [Section titled “SSML”](#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 [Section titled “Performance Tips”](#performance-tips) 1. **Pre-chunk large texts** for better control 2. **Keep `characterLimit` within range** (50–300; default 100) 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 [Section titled “Overview”](#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 [Section titled “Usage”](#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 [Section titled “Hook Signature”](#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 [Section titled “Return Value Semantics”](#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 [Section titled “Patterns”](#patterns) ### Static aliasing [Section titled “Static aliasing”](#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 [Section titled “Locale-based routing”](#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 [Section titled “Debug logging”](#debug-logging) Observe what selectors are being passed without changing behavior: ```typescript resolveVoice: (selector) => { console.log('[RV] resolving voice:', selector); return selector; }, ``` ## TypeScript [Section titled “TypeScript”](#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 [Section titled “By Name”](#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 [Section titled “By Pattern”](#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" } ``` ## By Query [Section titled “By Query”](#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 [Section titled “Direct Voice Override”](#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 [Section titled “Default Voice”](#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 [Section titled “Language-Aware Website Widgets”](#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. ## Force Fallback [Section titled “Force Fallback”](#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 [Section titled “Resolution Precedence”](#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 [Section titled “Voice Matching Chain”](#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 ## Listing Voices [Section titled “Listing Voices”](#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 [Section titled “Platform-Specific Voices”](#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 [Section titled “Language Codes”](#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 [Section titled “Example: Voice Selector UI”](#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 [Section titled “At a Glance”](#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 [Section titled “Add the Script and Initialize”](#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. ## Theme optional [Section titled “Theme ”](#theme) 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 [Section titled “Layout ”](#layout) Layout controls where the player appears and how much space it uses. | Config option | Default | What it does | | ---------------- | ---------- | ------------------------------------------------------------------------------- | | `position` | `'before'` | Mounts the player before, after, inline inside, or inside a custom 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 [Section titled “Controls ”](#controls) 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. | | `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 [Section titled “Behavior ”](#behavior) 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 [Section titled “Advanced ”](#advanced) 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 [Section titled “Place the Player in a specific container”](#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 [Section titled “Voice Override”](#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 [Section titled “Exclude From Narration”](#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 [Section titled “Testing”](#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 [Section titled “Dynamic Content”](#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 [Section titled “Troubleshooting”](#troubleshooting) ## Next Steps [Section titled “Next Steps”](#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. ## What ResponsiveVoice gives you [Section titled “What ResponsiveVoice gives you”](#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 [Section titled “How it compares”](#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 [Section titled “When to choose what”](#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 [Section titled “Next steps”](#next-steps) * [Installation](/getting-started/installation/) — add the script or install from npm. * [Voice Selection](/guides/voice-selection/) — filter and resolve voices. * [REST API Overview](/rest-api/overview/) — server-side synthesis. # Authentication > API key and secret authentication for the ResponsiveVoice REST API ## Authentication [Section titled “Authentication”](#authentication) Every REST API request is authenticated with two credentials sent as headers: * **`X-API-Key`** — your website's public identifier. * **`X-API-Secret`** — your non-public server credential. Both must be sent together: ```bash curl https://texttospeech.responsivevoice.org/v2/voices \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-API-Secret: YOUR_API_SECRET" ``` ### Getting your credentials [Section titled “Getting your credentials”](#getting-your-credentials) Both live in your website's settings in the [ResponsiveVoice dashboard](https://app.responsivevoice.org) (no account yet? [sign up](https://responsivevoice.org/register)): * **API key** — shown in the "Your site code" snippet (`key: XXXXX`). * **API secret** — created under "Server-to-server API secrets". It is shown **only once** when generated, so copy it immediately; you can revoke it there at any time. Caution The API secret is a credential — keep it server-side. Never embed it in browser code or commit it to source control. Browser apps use the [`@responsivevoice/core`](/getting-started/quick-start/) SDK, which authenticates by origin and does not need the secret. # Error Handling > Error responses and rate limits for the ResponsiveVoice REST API ## Error Responses [Section titled “Error Responses”](#error-responses) All errors return JSON with an `error` object. Validation errors add an `errors` array detailing each problem: ```json { "error": { "message": "Language code is required", "code": "VALIDATION_ERROR", "statusCode": 400, "errors": ["Provide \"lang\" or \"voice\" parameter"] } } ``` ### Error Codes [Section titled “Error Codes”](#error-codes) | HTTP Code | Code | Description | | --------- | ---------------------------- | -------------------------------------------------------------------------------------- | | 400 | VALIDATION\_ERROR | Invalid request parameters | | 401 | UNAUTHORIZED | Invalid or missing API key or secret | | 404 | NOT\_FOUND | Voice or resource not found | | 429 | RATE\_LIMIT\_EXCEEDED | Tier request-rate limit exceeded | | 429 | BURST\_RATE\_LIMIT\_EXCEEDED | Too many requests from one client in a short window | | 500 | INTERNAL\_ERROR | Server error | | 502 | *(none)* | Upstream TTS provider error; `message` carries the provider's failure, no `code` field | ## Rate Limits [Section titled “Rate Limits”](#rate-limits) Rate limits apply per API key, which is required — requests without a valid key are rejected. The same limits apply to both the v1 and v2 APIs. | Plan | Requests per minute | | ---------- | ------------------- | | Free | 100 | | Commercial | 1,000 | | Enterprise | Custom | The per-minute limits above are counted per API key. A separate burst check applies per API key **and** client IP over a short window, so unusually rapid bursts from one client can be limited briefly even while you're under the per-minute limit. ### Monthly character quota [Section titled “Monthly character quota”](#monthly-character-quota) | Plan | Characters per month | | ---------- | ------------------------------------- | | Free | 1,000,000 | | Commercial | Fair use — contact us for high volume | | Enterprise | Custom | Keys that exceed the monthly quota are suspended until upgraded. # Overview > Introduction to the ResponsiveVoice REST API ## ResponsiveVoice REST API [Section titled “ResponsiveVoice REST API”](#responsivevoice-rest-api) The ResponsiveVoice REST API generates speech audio from any language or platform — no website required. Use it for: * Server-side audio generation * Mobile apps (Android, iOS) and other non-browser clients * Batch processing of text to speech * Platforms without Web Speech API support **Base URL:** `https://texttospeech.responsivevoice.org/v2` ### Features [Section titled “Features”](#features) * **Multilingual built-in voice catalog** * **Streaming support** via HTTP audio streaming and WebSocket streaming * **BYOK premium providers** — bring your own provider API keys for premium voices * **CDN-cacheable** GET endpoint for efficient audio delivery * **HATEOAS navigation** links in voice responses ### Interactive Documentation [Section titled “Interactive Documentation”](#interactive-documentation) Explore the API interactively with the [Scalar API Reference](https://texttospeech.responsivevoice.org/reference). The full OpenAPI specification is available at [`/openapi.json`](https://texttospeech.responsivevoice.org/openapi.json) — import it into Postman, Insomnia, or other API tools. ### Endpoint Reference [Section titled “Endpoint Reference”](#endpoint-reference) Detailed endpoint documentation is auto-generated from the OpenAPI specification — see the [Endpoint Reference](/rest-api/reference/). V1 Deprecation V1 is deprecated. Only `GET /v1/text:synthesize` remains for backward compatibility with the legacy JS client. All new integrations should use the v2 endpoints documented in the [Endpoint Reference](/rest-api/reference/). # TypeScript SDK > Using @responsivevoice/api-client for typed TTS API access Official TypeScript client with full typing, retry logic, WebSocket streaming, and Node.js/browser support. Source: [`api-client`](https://github.com/responsivevoice/api-client). ## Installation [Section titled “Installation”](#installation) ```bash npm install @responsivevoice/api-client ``` ## Quick Start [Section titled “Quick Start”](#quick-start) ```typescript import { ResponsiveVoiceAPIClient } from '@responsivevoice/api-client'; const client = new ResponsiveVoiceAPIClient({ apiKey: process.env.RESPONSIVEVOICE_API_KEY, apiSecret: process.env.RESPONSIVEVOICE_API_SECRET, }); // Synthesize const audio = await client.synthesize({ text: 'Hello world', voice: 'UK English Female', format: 'mp3', }); console.log(audio.blob, audio.url, audio.format); // List voices (with optional filters) const { voices } = await client.getVoices({ lang: 'en-GB', gender: 'female' }); // Cancel a request const controller = new AbortController(); setTimeout(() => controller.abort(), 5_000); await client.synthesize( { text: 'Long text…', voice: 'UK English Female' }, { signal: controller.signal }, ); ``` ## Authentication [Section titled “Authentication”](#authentication) Server-side callers send an `apiKey` + `apiSecret` pair (the client attaches them as `X-API-Key` + `X-API-Secret`). Get both from your website's settings in the [ResponsiveVoice dashboard](https://app.responsivevoice.org) ([sign up](https://responsivevoice.org/register)): the **API key** from the "Your site code" snippet, and the **API secret** under "Server-to-server API secrets" (shown only once, revocable). Keep the secret server-side — browser apps use [`@responsivevoice/core`](/getting-started/quick-start/), which authenticates by origin and needs no secret. ## Runtime Notes [Section titled “Runtime Notes”](#runtime-notes) * **Node 18+**: works out of the box (native `fetch`, `Blob`, `AbortController`). * **Node 16–17**: pass a `fetch` polyfill via the `fetch` config option. * **Node < 22 + WebSocket streaming**: pass a `WebSocket` implementation (e.g. `ws`) to `WebSocketConnection`. ## Reference Documentation [Section titled “Reference Documentation”](#reference-documentation) * [API Reference (TypeDoc)](/api/api-client/src/) — complete typed API surface * [Package README on GitHub](https://github.com/responsivevoice/api-client#readme) — full walkthrough, all configuration options, error taxonomy, retry tuning