Skip to main content
Use headless mode when you need full control over the player interface—custom layouts, branded controls, or deep integration into your app—while the BeyondWords SDK still handles playback, streaming, analytics, and ads.
For most sites, player settings in the dashboard are enough. Only build a custom UI if you need a fundamentally different experience. For minor style tweaks, see style overrides below—but prefer dashboard settings where possible.

When to use headless mode

ApproachUse when
Player settingsColors, size, widget, highlighting—no code required
Style overridesSmall CSS tweaks to the built-in UI
Headless mode (this guide)Fully custom controls and layout
Direct API callsYou don’t want any player code (loses analytics, ads, segment playback, and more)

How it works

  1. Initialize the player with showUserInterface: false (or omit target to mount headlessly at the end of <body>).
  2. The SDK still loads content, manages the media element, and fires events.
  3. Your code reads player state (currentTime, playbackState, content, etc.) and renders your own UI.
  4. Your UI writes back to player properties (playbackState = 'playing', currentTime = 30, etc.).
The player always mounts in the DOM because playback uses a native media element (<video>). With playerStyle: "video" and showUserInterface: false, the video element renders without built-in controls.

Enable headless mode

Option 1—hide the built-in UI:
new BeyondWords.Player({
  projectId: YOUR_PROJECT_ID,
  contentId: 'YOUR_CONTENT_ID',
  target: '#my-player-mount',
  showUserInterface: false,
});
Option 2—omit target: If you don’t pass target, the player mounts at the end of <body> with the UI disabled automatically. On iOS and Android, set showUserInterface: false in PlayerSettings and build native controls that call the SDK setters. See the iOS and Android example apps.

Build a custom UI

The simplest pattern is event-driven re-rendering—listen for player changes, then read state and update your UI:
const player = new BeyondWords.Player({
  projectId: YOUR_PROJECT_ID,
  contentId: 'YOUR_CONTENT_ID',
  showUserInterface: false,
});

player.addEventListener('<any>', rerenderUI);

function rerenderUI() {
  const item = player.content[player.contentIndex];
  const isPlaying = player.playbackState === 'playing';
  // Update your DOM from player.currentTime, item.title, etc.
}
Use the <any> event type to re-render on every state change. See player events for specific event types and listener cleanup. For playback control, see programmatic control.

React example

import { useEffect, useState } from 'react';

function CustomPlayer({ player }) {
  const [, setTick] = useState(0);

  useEffect(() => {
    const listener = player.addEventListener('<any>', () => setTick(t => t + 1));
    return () => player.removeEventListener('<any>', listener);
  }, [player]);

  const item = player.content[player.contentIndex];
  const isPlaying = player.playbackState === 'playing';

  return (
    <div>
      <span>{item?.title ?? 'Loading...'}</span>
      <button onClick={() => { player.playbackState = isPlaying ? 'paused' : 'playing'; }}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <span>{Math.floor(player.currentTime / 60)}:{String(Math.floor(player.currentTime % 60)).padStart(2, '0')}</span>
    </div>
  );
}

WordPress

The WordPress plugin injects the player script on published posts. Use the beyondwords_player_script_onload filter to call your custom UI initialization after the player loads:
function my_beyondwords_player_script_onload( $onload, $params ) {
    return $onload . 'initializeCustomUserInterface();';
}
add_filter( 'beyondwords_player_script_onload', 'my_beyondwords_player_script_onload', 10, 2 );
The plugin injects the player script—do not add it manually. Then follow the plain JavaScript pattern above, using BeyondWords.Player.instances()[0] to get the player instance.

Plain JavaScript example

<div>
  <button id="play-button">Play</button>
  <span id="content-title">Loading...</span>
  <span id="time-indicator">0:00</span>
</div>

<script>
  var player, playButton, contentTitle, timeIndicator;

  function initializeCustomUserInterface() {
    player = BeyondWords.Player.instances()[0];
    player = player || new BeyondWords.Player({
      projectId: YOUR_PROJECT_ID,
      contentId: 'YOUR_CONTENT_ID',
      showUserInterface: false,
    });

    playButton = document.getElementById('play-button');
    contentTitle = document.getElementById('content-title');
    timeIndicator = document.getElementById('time-indicator');

    player.addEventListener('<any>', rerenderCustomUserInterface);
    playButton.addEventListener('click', playOrPause);
  }

  function rerenderCustomUserInterface() {
    var contentItem = player.content[player.contentIndex];
    var isPlaying = player.playbackState === 'playing';
    var minutes = Math.floor(player.currentTime / 60);
    var seconds = Math.floor(player.currentTime % 60);

    playButton.innerText = isPlaying ? 'Pause' : 'Play';
    contentTitle.innerText = contentItem ? contentItem.title : '';
    timeIndicator.innerText = minutes + ':' + seconds.toString().padStart(2, '0');
  }

  function playOrPause() {
    player.playbackState = player.playbackState === 'playing' ? 'paused' : 'playing';
  }
</script>

<script async defer
  src="https://proxy.beyondwords.io/npm/@beyondwords/player@latest/dist/umd.js"
  onload="initializeCustomUserInterface()">
</script>

Style overrides

If you only need minor tweaks to the built-in player UI—rather than a full custom interface—you can override CSS. Player styles are highly specific, so use the intentional override selector:
.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp.bwp .main {
  border-radius: 0 !important;
}
Player styles may change between releases. Monitor overrides after player updates, or prefer dashboard settings instead.

Segment highlight colors

Override segment detection highlight colors without the .bwp selector:
[data-beyondwords-marker]:nth-child(3n) .beyondwords-highlight {
  background: #fcc !important;
}

Direct API calls

You can call the player API directly and build a UI from the JSON response. This works, but you lose built-in features: We recommend headless mode over direct API calls unless you have a specific reason to bypass the SDK entirely. Currently, all player data comes from the player endpoints, which are public and cached for five minutes. You could call these yourself and build your own user interface on top of this data.

FAQs

Yes. Set showUserInterface: false in PlayerSettings on iOS or Android, then build native controls. The example apps in the iOS and Android repositories demonstrate this pattern.
No. Analytics events still fire when using the SDK in headless mode, as long as you use the player instance rather than calling the API directly.
Enable highlighting in Distribution → Player → Settings, then use segment detection markers in your article HTML. Your custom UI can listen for segment events and apply highlight styles, or use the segment CSS overrides with the built-in highlight classes.
Magic Embed installs the standard player automatically. Use headless mode when you need a bespoke interface that the dashboard player cannot provide.

Getting help

If you encounter issues or have questions, contact support. Include your initialization code and a description of the custom UI you’re building.