> ## Documentation Index
> Fetch the complete documentation index at: https://docs.responsivevoice.org/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/guides/app-features/#web-player). This guide covers the **code path** — installing, configuring, and overriding the player programmatically with `init()` and `mount()`.

For live examples, see the [Web Player example](/examples/web-player/) and [Web Player Customization](/examples/web-player-customization/) pages.

## At a Glance

1. Drop the CDN script in your page and call:
   ```javascript
   responsiveVoice.init({
     apiKey: 'YOUR_API_KEY',
     features: {
       webPlayer: { enabled: true },
     },
   });
   ```
2. Choose a **Theme**: `neutral`, `responsivevoice`, or custom color tokens.
3. Set the **Layout** if the player should appear somewhere other than before the article.
4. Choose **Controls** such as progress, time, skip, speed, brand, and mini-player settings.
5. Tune **Behavior** such as paragraph highlighting, click-to-jump, and skipping non-readable content.
6. Use **Advanced** only when you need custom selectors, voice override, or extra narration exclusions.

## Add the Script and Initialize

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

```html
<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.

> [!NOTE]
> `init()` is async because it fetches your website configuration. The player mounts once initialization resolves; you do not need to `await` anything for a static `<article>` already in the DOM.

## Theme (optional)

Theme controls the visual style of the player.

Two presets ship with the player, plus a custom-token path for brand colors. Tabs preserve your selection across pages.

**Neutral**

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

```js {4}
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 {4} mark="theme: 'responsivevoice'"
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 {6-10}
window.responsiveVoice.init({
  apiKey: 'YOUR_API_KEY',
  features: {
    webPlayer: {
      enabled: true,
      theme: {
        fill: '#10b981',
        track: '#d1fae5',
        accentSoft: '#d1fae5',
      },
    },
  },
});
```

The full nine-token palette is `bg`, `fg`, `muted`, `accent`, `accentSoft`, `hover`, `border`, `track`, and `fill`. The [customization picker](https://examples.responsivevoice.org/browser/web-player-customization/) lets you tune those values and copy the matching configuration.

## Layout (optional)

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

| Config option    | Default    | What it does                                                                                                   |
| ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `position`       | `'before'` | Keyword (`before` / `after` / `inline`) relative to `selector`, or `{ target, at }` for a custom mount target. |
| `layout.mode`    | `'shrink'` | `shrink` hugs the controls; `fill` spans the container.                                                        |
| `layout.display` | `'block'`  | `block` gives the player its own line; `inline` flows with surrounding content.                                |

```js {6} {8-9}
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 {6-9}
window.responsiveVoice.init({
  apiKey: 'YOUR_API_KEY',
  features: {
    webPlayer: {
      enabled: true,
      position: {
        target: '#article-player',
        at: 'inside',
      },
    },
  },
});
```

## Controls (optional)

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

| Config option          | Default         | What it does                                                                                              |
| ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------- |
| `controls.progress`    | `true`          | Shows progress through the readable content.                                                              |
| `controls.time`        | `true`          | Shows elapsed and total estimated time.                                                                   |
| `controls.skip`        | `true`          | Shows previous and next paragraph buttons.                                                                |
| `controls.speed`       | `true`          | Shows the speed cycle button.                                                                             |
| `controls.brand`       | `true`          | Shows the ResponsiveVoice brand icon and the "Powered by" line; use `{ icon, poweredBy }` to toggle each. |
| `miniPlayer.enabled`   | `true`          | Shows the mini-player when the main player is out of view.                                                |
| `miniPlayer.position`  | `'bottom-left'` | Places the mini-player in a viewport corner or custom offset.                                             |
| `miniPlayer.animation` | `'slide'`       | Uses `none`, `fade`, `slide`, or `pop`.                                                                   |

```js {7-11} {14-16}
window.responsiveVoice.init({
  apiKey: 'YOUR_API_KEY',
  features: {
    webPlayer: {
      enabled: true,
      controls: {
        progress: true,
        time: true,
        skip: true,
        speed: true,
        brand: true,
      },
      miniPlayer: {
        enabled: true,
        position: 'bottom-left',
        animation: 'slide',
      },
    },
  },
});
```

## Behavior (optional)

Behavior covers highlighting, click-to-jump, and skipping non-readable content.

| Config option                   | Default | What it does                                                                                |
| ------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
| `navigation.paragraphHighlight` | `true`  | Highlights the currently spoken element.                                                    |
| `navigation.paragraphClick`     | `true`  | Lets visitors click a paragraph to start reading there.                                     |
| `sanitize.enabled`              | `true`  | Excludes scripts, styles, form controls, embedded media, and hidden content from narration. |

```js {7-8} {10}
window.responsiveVoice.init({
  apiKey: 'YOUR_API_KEY',
  features: {
    webPlayer: {
      enabled: true,
      navigation: {
        paragraphHighlight: true,
        paragraphClick: true,
      },
      sanitize: { enabled: true },
    },
  },
});
```

Keep `sanitize.enabled` on for normal sites. Turning it off narrates raw matched text and is mainly useful for debugging unusual markup.

## Advanced (optional)

Advanced options are for site-specific markup and voice overrides.

Most websites can leave these at their defaults. Use them when the player needs to read a specific part of a page, skip extra elements, or use a different voice from the website default.

| Config option       | Default           | What it does                                                        |
| ------------------- | ----------------- | ------------------------------------------------------------------- |
| `selector`          | `'article'`       | CSS selector for the element that contains readable content.        |
| `paragraphSelector` | `'p, h2, h3, li'` | CSS selector for readable elements inside the container.            |
| `voice`             | Website default   | Optional voice selector for this player only.                       |
| `sanitize.exclude`  | `[]`              | Extra CSS selectors to skip in addition to the built-in exclusions. |

```js {6-8} {11}
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.

### Place the Player in a specific container

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:

```html {1} mark="article-player"
<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>
```

```js {6} mark="article-player"
window.responsiveVoice.webPlayer?.mount('#article-preview', {
  voice: 'UK English Female',
  theme: 'responsivevoice',
  paragraphSelector: 'h2, p',
  position: {
    target: '#article-player',
    at: 'inside',
  },
});
```

This pattern is useful for product pages too: place a short, representative article demo after trust proof such as customer logos, then let visitors try the same player their readers would receive.

### Voice Override

Without configuration, the player narrates with the website default voice. To override per player, add a `voice` field. It accepts a name string, a `RegExp`, or a structured query, exactly like the second argument to `speak()`.

```js {6-7}
window.responsiveVoice.init({
  apiKey: 'YOUR_API_KEY',
  features: {
    webPlayer: {
      enabled: true,
      voice: 'UK English Female',
      rate: 0.95,
    },
  },
});
```

The full grammar lives in [Voice Selection](/guides/voice-selection/). The same `voice`, `rate`, `pitch`, and `volume` fields are also valid as per-mount overrides on `mount()`.

### Exclude From Narration

Mark any element with `data-rv-skip` to keep it out of the narration. The player still highlights surrounding paragraphs; the skipped element is invisible to the reader's progression.

```html {3-5} mark="data-rv-skip"
<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.

## Testing

After integrating, verify on the real page — its markup determines what's found and narrated:

- Press play and confirm the selected voice sounds right.
- Confirm the configured controls (progress, time, skip, speed, brand) appear.
- Click a paragraph if `navigation.paragraphClick` is enabled.
- Confirm highlighting appears if `navigation.paragraphHighlight` is enabled.
- Scroll until the main player leaves view and confirm the mini-player appears if enabled.
- Add a known excluded element and confirm it is skipped.

## Dynamic Content

For SPAs and lazy-loaded sections, the article may not be in the DOM when `init()` runs, so the auto-discovery pass misses it. Use `webPlayer.mount()` to attach a player after the element is rendered:

```js {4} mark="'post-area'" mark="webPlayer?.mount('#post-area')"
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.

> [!WARNING]
> Always call `handle.unmount()` before you remove the host element from the
> DOM. The player keeps DOM listeners attached to the article and the document.
> A forgotten unmount will not crash anything, but it can leak event handlers
> across SPA navigations.

## Troubleshooting

### The player doesn't appear

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

### `mount()` returned `null`

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

### The wrong content is read, or paragraphs are missing

- `paragraphSelector` must match the elements you want narrated (default `p, h2, h3, li`).
- Sanitization drops scripts, styles, form controls, media, and `hidden` / `aria-hidden` nodes, plus anything matching `data-rv-skip` or your `sanitize.exclude` selectors; a paragraph left empty after sanitization is skipped.

### The mini-player doesn't appear

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

### Handlers leak across SPA navigations

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

## Next Steps

- [Live demo](https://examples.responsivevoice.org/browser/web-player/) — The basic integration running in your browser. Source on GitHub, ready to copy and adapt.
- [Customize live](https://examples.responsivevoice.org/browser/web-player-customization/) — Tweak every option in real time. The matching init() and mount() snippets update alongside the player.
- [Web Player example walkthrough](/examples/web-player/) — In-docs walkthrough of multi-mount, imperative mount, and skipped content.
- [Voice Selection](/guides/voice-selection/) — The selector grammar used by the voice field: name string, regex, or structured query.
