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

# Build your own UI

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](/docs-and-guides/analytics/analytics-dashboard), and [ads](/docs-and-guides/monetization/ads).

<Tip>
  For most sites, [player settings](/docs-and-guides/distribution/player/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](#style-overrides) below—but prefer dashboard settings where possible.
</Tip>

## When to use headless mode

| Approach                                                                    | Use when                                                                          |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [**Player settings**](/docs-and-guides/distribution/player/player-settings) | Colors, size, widget, highlighting—no code required                               |
| [**Style overrides**](#style-overrides)                                     | Small CSS tweaks to the built-in UI                                               |
| **Headless mode** (this guide)                                              | Fully custom controls and layout                                                  |
| [**Direct API calls**](#direct-api-calls)                                   | You 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](/docs-and-guides/distribution/player/developer-guides/player-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:**

```javascript theme={null}
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](/docs-and-guides/distribution/player/installation/ios-sdk) and [Android](/docs-and-guides/distribution/player/installation/android-sdk) 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:](/docs-and-guides/distribution/player/developer-guides/player-events#overview)

```javascript theme={null}
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](/docs-and-guides/distribution/player/developer-guides/player-events) for specific event types and listener cleanup.

For playback control, see [programmatic control](/docs-and-guides/distribution/player/developer-guides/programmatic-control).

### React example

```javascript theme={null}
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](/docs-and-guides/integrations/publishing-platforms/wordpress) injects the player script on published posts. Use the `beyondwords_player_script_onload` filter to call your custom UI initialization after the player loads:

```php theme={null}
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

```html theme={null}
<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:

```css theme={null}
.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](/docs-and-guides/distribution/player/player-settings) instead.

### Segment highlight colors

Override [segment detection](/docs-and-guides/distribution/player/developer-guides/segment-detection) highlight colors without the `.bwp` selector:

```css theme={null}
[data-beyondwords-marker]:nth-child(3n) .beyondwords-highlight {
  background: #fcc !important;
}
```

## Direct API calls

You can call the [player API](/api-reference/player/show-2) directly and build a UI from the JSON response. This works, but you lose built-in features:

* BeyondWords analytics and [analytics integrations](/docs-and-guides/analytics/analytics-integrations)
* [Ads](/docs-and-guides/monetization/ads) (pre/mid/post-roll)
* [Segment detection](/docs-and-guides/distribution/player/developer-guides/segment-detection) and paragraph playback
* Media Session API support
* Intro/outro and playlist handling

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](/api-reference/player/by-content-id), 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

<AccordionGroup>
  <Accordion title="Can I use headless mode with native apps?">
    Yes. Set `showUserInterface: false` in `PlayerSettings` on iOS or Android, then build native controls. The example apps in the [iOS](https://github.com/beyondwords-io/player-ios/tree/main/Example) and [Android](https://github.com/beyondwords-io/player-android/tree/main/example) repositories demonstrate this pattern.
  </Accordion>

  <Accordion title="Does headless mode disable analytics?">
    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.
  </Accordion>

  <Accordion title="How do I show paragraph highlighting with a custom UI?">
    Enable highlighting in **Distribution → Player → Settings**, then use [segment detection](/docs-and-guides/distribution/player/developer-guides/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](#segment-highlight-colors) with the built-in highlight classes.
  </Accordion>

  <Accordion title="Should I use headless mode or Magic Embed?">
    [Magic Embed](/docs-and-guides/integrations/magic-embed) installs the standard player automatically. Use headless mode when you need a bespoke interface that the dashboard player cannot provide.
  </Accordion>
</AccordionGroup>

## Getting help

If you encounter issues or have questions, [contact support](/docs-and-guides/support/get-support). Include your initialization code and a description of the custom UI you're building.
