Skip to content

Web Player

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. 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 and Web Player Customization pages.

  1. Drop the CDN script in your page and call:
    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.

Drop the CDN bundle into your page and turn the player on. By default it attaches to the first <article> element it finds and narrates every p, h2, h3, and li inside it.

<script src="https://cdn.responsivevoice.org/sdk/latest/responsivevoice.js"></script>
<script>
window.responsiveVoice.init({
apiKey: 'YOUR_API_KEY', // required — without it the player runs in demo mode (one voice)
features: {
webPlayer: { enabled: true },
},
});
</script>

If your page has an <article>, the player appears above it after initialization.

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.

The default theme is light, minimal, and designed to sit comfortably on most host pages. No extra configuration is needed:

window.responsiveVoice.init({
apiKey: 'YOUR_API_KEY',
features: {
webPlayer: { enabled: true },
},
});

Layout controls where the player appears and how much space it uses.

Config optionDefaultWhat 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.
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:

window.responsiveVoice.init({
apiKey: 'YOUR_API_KEY',
features: {
webPlayer: {
enabled: true,
position: {
target: '#article-player',
at: 'inside',
},
},
},
});

Controls decide which buttons are visible and whether the floating mini-player is enabled.

Config optionDefaultWhat it does
controls.progresstrueShows progress through the readable content.
controls.timetrueShows elapsed and total estimated time.
controls.skiptrueShows previous and next paragraph buttons.
controls.speedtrueShows the speed cycle button.
controls.brandtrueShows the ResponsiveVoice brand icon.
miniPlayer.enabledtrueShows 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.
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 covers highlighting, click-to-jump, and skipping non-readable content.

Config optionDefaultWhat it does
navigation.paragraphHighlighttrueHighlights the currently spoken element.
navigation.paragraphClicktrueLets visitors click a paragraph to start reading there.
sanitize.enabledtrueExcludes scripts, styles, form controls, embedded media, and hidden content from narration.
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 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 optionDefaultWhat 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.
voiceWebsite defaultOptional voice selector for this player only.
sanitize.exclude[]Extra CSS selectors to skip in addition to the built-in exclusions.
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 <article> inside another <article>, are filtered to the outermost element so duplicate players are not created.

Many WordPress themes and marketing sites wrap the entire page in an <article>. 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:

<div id="article-player"><!-- The player will be placed here --></div>
<div id="article-preview">
<h2>A better way to experience the web</h2>
<p>Visitors can listen while they commute, work, or rest their eyes.</p>
<p>The current passage is highlighted as the article is read.</p>
</div>
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.

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().

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. The same voice, rate, pitch, and volume fields are also valid as per-mount overrides on mount().

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.

<article>
<p>Read this paragraph.</p>
<aside class="pullquote" data-rv-skip>
Decorative pull-quote, not narrated.
</aside>
<p>Continue from here.</p>
</article>

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.

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.

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:

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.

  • Confirm init() ran and resolved, and features.webPlayer.enabled is true.
  • Confirm selector matches an element that is already in the DOM when init() runs. Content rendered later (SPA, lazy-load) is missed by the one-time auto-discovery pass — mount it explicitly with mount().

It returns null when the feature isn't enabled or the target element isn't in the DOM — which is why the ?. guards matter. Confirm enabled: true and that the element exists before the call.

The wrong content is read, or paragraphs are missing

Section titled “The wrong content is read, or paragraphs are missing”
  • paragraphSelector must match the elements you want narrated (default p, h2, h3, li).
  • Sanitization drops scripts, styles, form controls, media, and hidden / aria-hidden nodes, plus anything matching data-rv-skip or your sanitize.exclude selectors; a paragraph left empty after sanitization is skipped.

miniPlayer.enabled must be true, and it only surfaces while playback is active and the main player has scrolled out of view.

Call handle.unmount() before removing the host element — the player keeps listeners on the article and document until you do.