Skip to content

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.

  • ThemeNeutral, 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.

The example panel keeps a single mount handle and replaces it on every change:

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.

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):

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.

The floating mini-player surfaces when the main player scrolls out of view. Its viewport corner is configurable via miniPlayer.position:

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

miniPlayer.animation controls how the mini-player enters and leaves as you scroll:

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.

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:

rv.webPlayer?.mount('#article', {
voice: 'US English Male',
rate: 0.9,
});

voice accepts the full VoiceSelector 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:

// 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:

// 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,
});

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:

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.

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):

FieldWhen omitted
voiceInherits the website default voice's name as a selector
pitchInherits the website default pitch
rateInherits the website default rate
volumeInherits the website default volume