> ## 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 Customization

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.

> [!TIP]
> [Live demo](https://examples.responsivevoice.org/browser/web-player-customization/) · [Source code](https://github.com/responsivevoice/examples/tree/main/browser/web-player-customization)

## 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

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

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

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

`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

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

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

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

- [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.
