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

# Theming

> Style the components to match your brand by editing the code you own.

export const MinimalPlayerDemo = () => {
  const newEvent = props => ({
    id: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    status: "pending",
    initiatedBy: "user",
    ...props
  });
  const formatTime = time => {
    if (time < 0) time = Math.max(0, time);
    const hours = Math.floor(time / 3600).toString();
    let minutes = Math.floor(time % 3600 / 60).toString();
    let seconds = Math.floor(time % 60).toString();
    if (seconds.length < 2) seconds = `0${seconds}`;
    if (time < 3600) return `${minutes}:${seconds}`;
    if (minutes.length < 2) minutes = `0${minutes}`;
    return `${hours}:${minutes}:${seconds}`;
  };
  const RATES = Array.from({
    length: 12
  }, (_, i) => Math.round((0.9 + i * 0.1) * 10) / 10);
  const [target, setTarget] = useState(null);
  const [player, setPlayer] = useState(null);
  const [playbackState, setPlaybackState] = useState("stopped");
  const [callToAction, setCallToAction] = useState(null);
  const [currentTime, setCurrentTime] = useState(0);
  const [playbackRate, setPlaybackRate] = useState(1);
  useEffect(() => {
    if (!target) return;
    let instance;
    const init = () => {
      instance = new window.BeyondWords.Player({
        target,
        projectId: 9504,
        contentId: "ba00ef51-03ac-4a85-8b98-e45c957e8ef8",
        widgetStyle: "none",
        showUserInterface: false,
        analyticsConsent: "none"
      });
      const sync = () => {
        setPlaybackState(instance.playbackState);
        setCallToAction(instance.callToAction);
        setCurrentTime(instance.currentTime);
        setPlaybackRate(instance.playbackRate);
      };
      instance.addEventListener("<any>", sync);
      sync();
      setPlayer(instance);
    };
    if (window.BeyondWords) init(); else window.addEventListener("BeyondWordsReady", init);
    return () => {
      window.removeEventListener("BeyondWordsReady", init);
      if (instance) instance.destroy();
    };
  }, [target]);
  const isPlaying = playbackState === "playing";
  const stopped = playbackState === "stopped";
  const rateMenuId = useId();
  const rateMenuRef = useRef(null);
  const [rateMenuOpen, setRateMenuOpen] = useState(false);
  useEffect(() => {
    if (!rateMenuOpen) return;
    const onPointerDown = event => {
      if (!rateMenuRef.current?.contains(event.target)) setRateMenuOpen(false);
    };
    const onKeyDown = event => {
      if (event.key === "Escape") setRateMenuOpen(false);
    };
    document.addEventListener("pointerdown", onPointerDown);
    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("pointerdown", onPointerDown);
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [rateMenuOpen]);
  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'
  }}>
      <div ref={setTarget} style={{
    display: "none"
  }} />
      {player && <div style={{
    display: "flex",
    width: "536px",
    alignItems: "center",
    gap: "0.75rem",
    color: "#171717"
  }}>
          <button type="button" aria-label={isPlaying ? "Pause audio" : "Play audio"} onClick={() => {
    const name = isPlaying ? "Pause" : "Play";
    player.onEvent(newEvent({
      type: `Pressed${name}`,
      description: `The ${name.toLowerCase()} button was pressed.`
    }));
  }} style={{
    display: "flex",
    width: "2rem",
    height: "2rem",
    flexShrink: 0,
    cursor: "pointer",
    alignItems: "center",
    justifyContent: "center",
    background: "none",
    border: "none",
    padding: 0,
    color: "inherit"
  }}>
            {isPlaying ? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={32} height={32} fill="currentColor" aria-hidden="true">
                <path fillRule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20ZM8.7 8.95a1.05 1.05 0 0 1 2.1 0v6.1a1.05 1.05 0 0 1 -2.1 0ZM13.2 8.95a1.05 1.05 0 0 1 2.1 0v6.1a1.05 1.05 0 0 1 -2.1 0Z" />
              </svg> : <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={32} height={32} fill="currentColor" aria-hidden="true">
                <path fillRule="evenodd" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20ZM10.45 8.48 Q9.4 7.9 9.4 9.1 L9.4 14.9 Q9.4 16.1 10.45 15.52 L15.4 12.78 Q16.8 12 15.4 11.22Z" />
              </svg>}
          </button>
          {stopped ? <div style={{
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap",
    fontSize: "1rem",
    lineHeight: "1.5rem",
    fontWeight: 500
  }}>
              {callToAction ?? "Listen to article"}
            </div> : <>
              <button type="button" aria-label="Skip back 10 seconds" onClick={() => player.onEvent(newEvent({
    type: "PressedSeekBack",
    description: "The seek backward button was pressed.",
    seconds: 10
  }))} style={{
    flexShrink: 0,
    cursor: "pointer",
    background: "none",
    border: "none",
    padding: 0,
    color: "inherit"
  }}>
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={28} height={28} fill="none" aria-hidden="true">
                  <path d="M7 9l-3 -3l3 -3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M15.997 17.918a6.002 6.002 0 0 0 -.997 -11.918h-11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M6 14v6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M9 15.5v3a1.5 1.5 0 0 0 3 0v-3a1.5 1.5 0 0 0 -3 0" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </button>
              <div ref={rateMenuRef} style={{
    position: "relative",
    flexShrink: 0
  }}>
                <button type="button" aria-haspopup="listbox" aria-expanded={rateMenuOpen} aria-controls={rateMenuId} onClick={() => setRateMenuOpen(value => !value)} style={{
    cursor: "pointer",
    borderRadius: "0.375rem",
    padding: "0.25rem 0.5rem",
    fontSize: "0.875rem",
    fontWeight: 500,
    fontVariantNumeric: "tabular-nums",
    background: "none",
    border: "none",
    color: "inherit"
  }}>
                  {playbackRate}x
                </button>
                {rateMenuOpen && <div id={rateMenuId} role="listbox" aria-label="Playback speed" style={{
    position: "absolute",
    top: "100%",
    left: 0,
    zIndex: 50,
    marginTop: "0.5rem",
    minWidth: "9rem",
    overflow: "hidden",
    borderRadius: "0.75rem",
    background: "#171717",
    padding: "0.25rem 0",
    color: "#fff",
    boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)"
  }}>
                    <ul style={{
    maxHeight: "18rem",
    overflow: "auto",
    margin: 0,
    padding: 0,
    listStyle: "none"
  }}>
                      {RATES.map(rate => {
    const selected = Math.abs(rate - playbackRate) < 0.001;
    return <li key={rate} role="option" aria-selected={selected}>
                            <button type="button" onClick={() => {
      player.playbackRate = rate;
      setRateMenuOpen(false);
    }} style={{
      display: "flex",
      width: "100%",
      cursor: "pointer",
      alignItems: "center",
      justifyContent: "space-between",
      gap: "1.5rem",
      padding: "0.5rem 1rem",
      textAlign: "left",
      fontSize: "0.875rem",
      fontVariantNumeric: "tabular-nums",
      color: "#fff",
      background: selected ? "rgb(255 255 255 / 0.05)" : "transparent",
      border: "none",
      fontWeight: selected ? 500 : 400
    }}>
                              <span>{rate}x</span>
                              {selected && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width={16} height={16} fill="currentColor" aria-hidden="true">
                                  <path fillRule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0l-3.5-3.5a1 1 0 1 1 1.4-1.4L8.5 12l6.8-6.7a1 1 0 0 1 1.4 0Z" />
                                </svg>}
                            </button>
                          </li>;
  })}
                    </ul>
                  </div>}
              </div>
              <div style={{
    flexShrink: 0,
    fontSize: "1rem",
    fontVariantNumeric: "tabular-nums"
  }}>{formatTime(currentTime)}</div>
            </>}
        </div>}
    </div>;
};

The components are deliberately minimal and unopinionated. There's no `theme`
prop and no set of CSS variables to override. Because the CLI copies the source
into your project, **you own the code, so styling means editing it**: change its
classes, swap its icons, restructure its markup. Nothing is hidden behind a
package boundary.

This page shows the patterns. The [Minimal Player](/player-ui/react/minimal-player)
example below is a full worked walkthrough.

## Where the styles live

After you add a component, its source sits in your project (by default under
`@/registry/react/`). Open the file and you'll find ordinary JSX with a default
inline SVG icon and, where needed, a little Tailwind. For example, the
[Play Pause Button](/player-ui/react/play-pause-button) renders a bare button with the
default play / pause icon:

```tsx PlayPauseButton.tsx theme={null}
<button onClick={/* emits PressedPlay / PressedPause */}>
  {isPlaying ? <svg>{/* default pause icon */}</svg> : <svg>{/* default play icon */}</svg>}
</button>
```

## Edit the classes in place

To theme it, add the classes you want and swap the icons for your own. For
example, restyle the same component into a round brand-purple button:

```tsx PlayPauseButton.tsx theme={null}
<button
  className="flex size-12 items-center justify-center rounded-full bg-[#943bfc] text-white cursor-pointer hover:opacity-90"
  onClick={/* emits PressedPlay / PressedPause */}
>
  {playbackState === 'playing' ? <PauseIcon /> : <PlayIcon />}
</button>
```

Use your own design tokens instead of arbitrary values (`bg-[#943bfc]`) wherever
you can, such as `bg-brand` or `bg-primary` from your Tailwind theme. This keeps
the player in sync with the rest of your app.

## Swap the icons

Each component ships a **default inline SVG** icon (the same art the real
BeyondWords player uses). It's just markup in the file you own, so replace it
with your own SVG, an icon library, or text. The button reads `playbackState`,
so you decide what each state renders:

```tsx theme={null}
{isPlaying ? (
  <svg>{/* your pause icon */}</svg>
) : (
  <svg>{/* your play icon */}</svg>
)}
```

The icons use `fill="currentColor"`, so by default they take the surrounding
text color. Set a color on the button (or the icon) to change them.

## Conditional classes with `cn`

For state-dependent styling, the [`cn`](https://github.com/dcastil/tailwind-merge)
helper (installed at `@/lib/utils`) merges class lists and resolves Tailwind
conflicts. The examples use it to restyle themselves when they detach into a
floating widget:

```tsx theme={null}
import { cn } from '@/lib/utils';

<div
  className={cn(
    'flex h-12 items-center gap-2 rounded-3xl bg-[#F5F5F5] p-1',
    mode === 'widget' && 'fixed right-4 bottom-4 z-50 shadow-md',
  )}
>
```

## Dark mode

There's no dark-mode toggle baked in. Use Tailwind's `dark:` variant so the
player follows your app's existing color scheme:

```tsx theme={null}
<div className="bg-white text-black dark:bg-zinc-900 dark:text-white">
```

## Primitives vs. examples

* **Components** (primitives) are minimal and unopinionated on purpose. The
  [Progress Bar](/player-ui/react/progress-bar), for instance, ships just enough
  Tailwind to be usable: a translucent track with a solid fill. Override
  those classes freely.
* **Examples** are fully styled, pre-assembled players. Read them as worked
  references, or copy one and restyle it rather than starting from scratch.

## Worked example: Minimal Player

The Minimal example takes the same components shown throughout these docs and
restyles and recomposes them into a clean "listen to article" strip in the
style of news-site players: a transparent single-line bar with no background
pill. While stopped it shows only a play button and the call to action; once
playback starts it swaps in a skip-back 10s control, a 0.9x–2x speed menu, and
the current time. Nothing about the player engine changed. Only the classes,
icons and layout did.

<MinimalPlayerDemo />

Use it as a template. Copy it, restyle it to match your brand, and you have a
custom player without touching playback, analytics, ads or segments.
