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

# Playlist

> Render the player's content list and let users jump between items.

export const WebComponentsDemo = ({name}) => {
  const MOCK_CONTENT = [{
    id: "demo-1",
    title: "How AI voices are changing publishing",
    imageUrl: "/images/player-ui/demo-cover.svg",
    sourceUrl: "https://beyondwords.io",
    sourceId: "demo",
    adsEnabled: false,
    duration: 124,
    audio: [{
      id: "audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 124
    }],
    video: [],
    summarization: {
      audio: [],
      video: []
    },
    segments: []
  }];
  const MOCK_PLAYLIST = [{
    id: "7a974fb1-1b89-4be3-a25d-4ad286d5305b",
    title: "Part 1: The newsroom shift",
    imageUrl: "/images/player-ui/demo-cover.svg",
    sourceUrl: "https://beyondwords.io",
    sourceId: "demo",
    adsEnabled: false,
    duration: 124,
    audio: [{
      id: "audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 124
    }],
    video: [],
    summarization: {
      audio: [],
      video: []
    },
    segments: []
  }, {
    id: "129e7681-fa4d-4526-8be9-d27c946a5023",
    title: "Part 2: Voices at scale",
    imageUrl: "/images/player-ui/demo-cover.svg",
    sourceUrl: "https://beyondwords.io",
    sourceId: "demo",
    adsEnabled: false,
    duration: 98,
    audio: [{
      id: "audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 98
    }],
    video: [],
    summarization: {
      audio: [],
      video: []
    },
    segments: []
  }, {
    id: "e9f2257c-14cf-46c0-bab6-bcaa9e229775",
    title: "Part 3: What comes next",
    imageUrl: "/images/player-ui/demo-cover.svg",
    sourceUrl: "https://beyondwords.io",
    sourceId: "demo",
    adsEnabled: false,
    duration: 152,
    audio: [{
      id: "audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 152
    }],
    video: [],
    summarization: {
      audio: [],
      video: []
    },
    segments: []
  }];
  const MOCK_ADVERTS = [{
    id: "advert-1",
    type: "native",
    placement: "pre-roll",
    vastUrl: null,
    clickThroughUrl: "https://sponsor.example.com",
    imageUrl: "/images/player-ui/demo-cover.svg",
    textColor: "#ffffff",
    backgroundColor: "#943bfc",
    iconColor: "#ffffff",
    audio: [{
      id: "advert-audio-1",
      url: "/images/player-ui/demo-silent.mp3",
      contentType: "audio/mpeg",
      duration: 8
    }],
    video: []
  }];
  const content = JSON.stringify(MOCK_CONTENT).replace(/'/g, "&#39;");
  const playlistContent = JSON.stringify(MOCK_PLAYLIST).replace(/'/g, "&#39;");
  const adverts = JSON.stringify(MOCK_ADVERTS).replace(/'/g, "&#39;");
  const DEMOS = {
    "play-pause-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-play-pause-button></bw-play-pause-button></bw-player-provider>`,
    "time-indicator": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-time-indicator></bw-time-indicator></bw-player-provider>`,
    "prev-segment-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-prev-segment-button></bw-prev-segment-button></bw-player-provider>`,
    "next-segment-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-next-segment-button></bw-next-segment-button></bw-player-provider>`,
    "prev-seconds-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-prev-seconds-button></bw-prev-seconds-button></bw-player-provider>`,
    "next-seconds-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-next-seconds-button></bw-next-seconds-button></bw-player-provider>`,
    "playback-rate-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-playback-rate-button></bw-playback-rate-button></bw-player-provider>`,
    "content-title": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-content-title></bw-content-title></bw-player-provider>`,
    "call-to-action": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-call-to-action></bw-call-to-action></bw-player-provider>`,
    "download-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-download-button></bw-download-button></bw-player-provider>`,
    "beyondwords-logo": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><bw-beyondwords-logo></bw-beyondwords-logo></bw-player-provider>`,
    "prev-track-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${playlistContent}'><bw-prev-track-button></bw-prev-track-button></bw-player-provider>`,
    "next-track-button": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${playlistContent}'><bw-next-track-button></bw-next-track-button></bw-player-provider>`,
    "playlist": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${playlistContent}'><bw-playlist></bw-playlist></bw-player-provider>`,
    "feature-image": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><div style="width:100%;max-width:20rem;min-width:15rem;overflow:hidden;border-radius:0.5rem"><bw-feature-image></bw-feature-image></div></bw-player-provider>`,
    "progress-bar": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}'><div style="display:flex;flex-direction:row;align-items:center;min-width:30rem"><bw-progress-bar></bw-progress-bar></div></bw-player-provider>`,
    "advert-link": `<bw-player-provider data-show-user-interface="false" data-analytics-consent="none" data-content='${content}' data-adverts='${adverts}' data-advert-index="0"><bw-advert-link></bw-advert-link></bw-player-provider>`,
    "standard-player": `<bw-standard-player-provider data-show-user-interface="false" data-analytics-consent="none" data-project-id="9504" data-content-id="ba00ef51-03ac-4a85-8b98-e45c957e8ef8"><bw-standard-player-bar><bw-standard-play-pause-button></bw-standard-play-pause-button><bw-standard-player-info><bw-standard-call-to-action></bw-standard-call-to-action><bw-standard-duration></bw-standard-duration></bw-standard-player-info><bw-standard-playback-controls><bw-standard-playback-rate-button></bw-standard-playback-rate-button><bw-standard-prev-segment-button></bw-standard-prev-segment-button><bw-standard-next-segment-button></bw-standard-next-segment-button><bw-standard-time-indicator></bw-standard-time-indicator><bw-standard-progress-bar></bw-standard-progress-bar><bw-standard-close-widget-button></bw-standard-close-widget-button></bw-standard-playback-controls><bw-standard-beyondwords-logo></bw-standard-beyondwords-logo></bw-standard-player-bar></bw-standard-player-provider>`,
    "minimal-player": `<bw-minimal-player-provider data-show-user-interface="false" data-analytics-consent="none" data-project-id="9504" data-content-id="ba00ef51-03ac-4a85-8b98-e45c957e8ef8"><bw-minimal-player-bar><bw-minimal-play-pause-button></bw-minimal-play-pause-button><bw-minimal-call-to-action></bw-minimal-call-to-action><bw-minimal-prev-seconds-button data-seconds="10"></bw-minimal-prev-seconds-button><bw-minimal-playback-rate-menu></bw-minimal-playback-rate-menu><bw-minimal-current-time></bw-minimal-current-time></bw-minimal-player-bar></bw-minimal-player-provider>`,
    "playlist-player": `<bw-playlist-player-provider data-show-user-interface="false" data-analytics-consent="none" data-project-id="9504" data-playlist-id="153102"><bw-playlist-player-root><bw-playlist-player-bar><bw-playlist-play-pause-button></bw-playlist-play-pause-button><bw-playlist-player-info><bw-playlist-call-to-action></bw-playlist-call-to-action><bw-playlist-duration></bw-playlist-duration></bw-playlist-player-info><bw-playlist-playback-controls><bw-playlist-playback-rate-button></bw-playlist-playback-rate-button><bw-playlist-prev-track-button></bw-playlist-prev-track-button><bw-playlist-next-track-button></bw-playlist-next-track-button><bw-playlist-time-indicator></bw-playlist-time-indicator><bw-playlist-progress-bar></bw-playlist-progress-bar></bw-playlist-playback-controls><bw-playlist-toggle-button></bw-playlist-toggle-button></bw-playlist-player-bar><bw-playlist-list></bw-playlist-list></bw-playlist-player-root></bw-playlist-player-provider>`,
    "video-player": `<bw-video-player-provider data-show-user-interface="false" data-analytics-consent="none" data-video="true" data-project-id="54009" data-content-id="5ab6a5e1-689c-4103-962d-e7e68a4258ff"><bw-video-feature-image></bw-video-feature-image><bw-video-control-bar><bw-video-prev-seconds-button data-color="white" data-seconds="10"></bw-video-prev-seconds-button><bw-video-play-pause-button data-color="white" data-size="56"></bw-video-play-pause-button><bw-video-next-seconds-button data-color="white" data-seconds="10"></bw-video-next-seconds-button></bw-video-control-bar><bw-video-widget-bar><bw-video-play-pause-button></bw-video-play-pause-button><bw-video-widget-controls><bw-video-playback-rate-button></bw-video-playback-rate-button><bw-video-prev-seconds-button data-seconds="10"></bw-video-prev-seconds-button><bw-video-next-seconds-button data-seconds="10"></bw-video-next-seconds-button><bw-video-time-indicator></bw-video-time-indicator><bw-video-progress-bar></bw-video-progress-bar><bw-video-close-widget-button></bw-video-close-widget-button></bw-video-widget-controls></bw-video-widget-bar></bw-video-player-provider>`,
    "vertical-video-player": `<bw-video-player-provider data-show-user-interface="false" data-analytics-consent="none" data-video="true" data-aspect="9/16" data-height="480" data-project-id="54009" data-content-id="5ab6a5e1-689c-4103-962d-e7e68a4258ff"><bw-video-feature-image></bw-video-feature-image><bw-video-control-bar><bw-video-prev-seconds-button data-color="white" data-seconds="10"></bw-video-prev-seconds-button><bw-video-play-pause-button data-color="white" data-size="56"></bw-video-play-pause-button><bw-video-next-seconds-button data-color="white" data-seconds="10"></bw-video-next-seconds-button></bw-video-control-bar><bw-video-widget-bar><bw-video-play-pause-button></bw-video-play-pause-button><bw-video-widget-controls><bw-video-playback-rate-button></bw-video-playback-rate-button><bw-video-prev-seconds-button data-seconds="10"></bw-video-prev-seconds-button><bw-video-next-seconds-button data-seconds="10"></bw-video-next-seconds-button><bw-video-time-indicator></bw-video-time-indicator><bw-video-progress-bar></bw-video-progress-bar><bw-video-close-widget-button></bw-video-close-widget-button></bw-video-widget-controls></bw-video-widget-bar></bw-video-player-provider>`
  };
  const markup = DEMOS[name];
  return <div className="not-prose flex w-full items-center justify-center rounded-lg border p-10" style={{
    minHeight: "16rem",
    fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
  }}>
      {markup ? <div dangerouslySetInnerHTML={{
    __html: markup
  }} /> : <p className="text-sm">No preview available for “{name}”.</p>}
    </div>;
};

<Tabs>
  <Tab title="Preview">
    <WebComponentsDemo name="playlist" />
  </Tab>

  <Tab title="Code">
    ```js Playlist.js theme={null}
    import { newEvent } from "@/lib/newEvent";
    import { nowPlayingMarkerSvg } from "@/registry/html/icons";
    import { BwPlayerConsumer } from "@/registry/html/PlayerProvider";

    export class BwPlaylist extends BwPlayerConsumer {
      _list = null;
      _listener = null;
      _content = null;
      _contentIndex = null;

      constructor() {
        super();
        this._list = document.createElement("ul");
        this._list.style.listStyle = "none";
        this._list.style.margin = "0";
        this._list.style.padding = "0";
      }

      #render() {
        const content = this._player?.content ?? [];
        const contentIndex = this._player?.contentIndex;

        if (content === this._content && contentIndex === this._contentIndex) return;
        this._content = content;
        this._contentIndex = contentIndex;

        this._list.replaceChildren(
          ...content.map((item, index) => {
            const btn = document.createElement("button");
            btn.setAttribute("aria-current", contentIndex === index);
            btn.setAttribute("aria-label", `Play ${item.title}`);
            btn.style.display = "flex";
            btn.style.alignItems = "center";
            btn.style.gap = "0.5rem";
            btn.style.width = "100%";
            btn.addEventListener("click", () => {
              if (!this._player) return;
              this._player.onEvent(
                newEvent({
                  type: "PressedPlaylistItem",
                  description: "A playlist item was pressed.",
                  index,
                }),
              );
            });

            const marker = document.createElement("span");
            marker.style.display = "inline-flex";
            marker.style.width = "1rem";
            marker.style.justifyContent = "center";
            if (contentIndex === index) {
              marker.innerHTML = nowPlayingMarkerSvg(16);
            } else {
              marker.textContent = index + 1;
            }

            const title = document.createElement("span");
            title.textContent = item.title;

            btn.append(marker, title);

            const li = document.createElement("li");
            li.appendChild(btn);
            return li;
          }),
        );
      }

      connectedCallback() {
        super.connectedCallback();
        this._listener = this._player?.addEventListener("<any>", () => this.#render());
        this.#render();
        this.appendChild(this._list);
      }

      disconnectedCallback() {
        this._player?.removeEventListener("<any>", this._listener);
        this._listener = null;
        super.disconnectedCallback();
      }
    }

    customElements.define("bw-playlist", BwPlaylist);
    ```
  </Tab>
</Tabs>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npx @beyondwords/cli components add playlist --html
  ```

  ```bash pnpm theme={null}
  pnpm dlx @beyondwords/cli components add playlist --html
  ```

  ```bash yarn theme={null}
  yarn dlx @beyondwords/cli components add playlist --html
  ```

  ```bash bun theme={null}
  bunx --bun @beyondwords/cli components add playlist --html
  ```
</CodeGroup>

## Usage

Import the modules (each calls `customElements.define`), then use the elements. Nest the playlist inside a `<bw-player-provider>`.

```html theme={null}
<bw-player-provider
  data-project-id="9504"
  data-playlist-id="153102"
  data-show-user-interface="false"
>
  <bw-playlist></bw-playlist>
</bw-player-provider>

<script type="module">
  import "@/registry/html/PlayerProvider";
  import "@/registry/html/Playlist";
</script>
```

`<bw-playlist>` finds its nearest
[`<bw-player-provider>`](/player-ui/html/player-provider) ancestor and maps over
its `content`. It renders a button per item and marks the active one
(`contentIndex`) with a now-playing icon. Pressing an item emits a `PressedPlaylistItem`
event (built with [`newEvent`](/player-ui/html/new-event)) carrying its `index`.

<Info>
  The event is emitted rather than setting `player.contentIndex` directly. This
  lets the player handle ads and outros correctly when switching tracks.
</Info>

<Warning>
  These components expect the BeyondWords player to be available as a global
  `BeyondWords` (the player UMD script). Load it before the provider connects.
</Warning>

## Reference

|                       |                                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------------- |
| **Attributes**        | None                                                                                                  |
| **Player state read** | `content`, `contentIndex`                                                                             |
| **Events emitted**    | `PressedPlaylistItem` (carries the item's `index`)                                                    |
| **Requires**          | a [`<bw-player-provider>`](/player-ui/html/player-provider) ancestor and the player UMD global loaded |
