# Renderer

`@braccato/core` is a custom element, `<braccato-lyrics>`, that renders synced lyrics, highlights each syllable as it is sung, and scrolls to follow the song. It has no runtime dependencies. This page covers what most integrations need. The [package README](https://github.com/better-lyrics/braccato/tree/master/packages/core#readme) has the full reference.

## Install

```bash
npm i @braccato/core
```

## Imports

Importing `@braccato/core/element` registers the element. The package does not load its own CSS, so import the three stylesheets too. Load `variables.css` first, because the other two read from it.

```javascript
import "@braccato/core/element";
import "@braccato/core/styles/variables.css";
import "@braccato/core/styles/lyrics.css";
import "@braccato/core/styles/instrumental.css";
```

Without the stylesheets, the lines are in the document but unstyled. The element renders into light DOM, not a shadow root, so your page CSS reaches the lines.

## Required CSS

The element has no `display` of its own. Set it:

```css
braccato-lyrics {
  display: block;
}
```

The renderer measures the space the first line needs to reach the scroll position, but `lyrics.css` only applies the bottom half. Add the top:

```css
.blyrics-container {
  padding-top: var(--blyrics-padding-top, 2rem);
}
```

## Scrolling

Autoscroll writes `scrollTop` on the nearest scrollable ancestor, starting at the element itself. An ancestor counts when its `overflow-y` is `auto` or `scroll`. If nothing scrolls, the renderer scrolls the whole page.

To make the element its own scroller, give it a height:

```css
braccato-lyrics {
  display: block;
  height: 60vh;
  overflow-y: auto;
}
```

To scroll a different container, name it with `host`. Writing `host` rebuilds the view, so set it once during setup.

```javascript
view.host = { getScrollElement: () => frame };
```

### Pausing autoscroll when the user scrolls

The element does not listen for scroll events. Tell the renderer when the user scrolls, or autoscroll pulls the view back on the next frame and `braccato:scroll-state` never fires:

```javascript
view.addEventListener("scroll", () => view.renderer?.noteUserScroll(), { passive: true });
```

Listen on whichever element actually scrolls.

## Driving playback

### From a media element

Set `source` to a CSS selector or to an `HTMLMediaElement`. A selector is resolved when the element connects, so the media element must already be in the document, or you set the property from script. While a source is bound, the element reads the time, paused state and playback rate on its own, and a line click seeks the media element. Writes to `currentTime` and `playing` are ignored in this mode.

```javascript
view.source = document.querySelector("audio");
```

### From your own clock

Without a source, write `currentTime` (in **seconds**) and `playing` on every animation frame. If the song plays at a rate other than 1, pass it as `tickOptions.playbackRate`.

```javascript
function tick() {
  view.currentTime = player.currentTimeSeconds;
  view.playing = !player.paused;
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
```

## Properties

You can write every property before the element is in the document.

| Property | Type | Description |
| --- | --- | --- |
| `lyrics` | `Lyric[] \| null` | The lines to render, usually from `@braccato/parsers`. An empty array clears the view |
| `lyricsOptions` | object | Build options: `loaderVisible`, `noLyrics`, `language` (a BCP 47 tag for the source lyrics) |
| `source` | `string \| HTMLMediaElement \| null` | The media element to follow, or a selector for it |
| `currentTime` | `number` | Playback position in seconds |
| `playing` | `boolean` | Whether the song is playing. A paused view animates differently |
| `tickOptions` | object | Clock offsets, passive scrolling, sample time and `playbackRate` |
| `theme` | `string` | A CSS stylesheet. See Theming |
| `host` | object | Overrides for what the renderer asks of the page, such as `getScrollElement` |
| `renderer` | read-only | The renderer underneath, for methods like `noteUserScroll()` |
| `status` | read-only | `idle`, `rendering`, `theme-conflict` or `no-browsing-context` |

The `source`, `theme`, `current-time` and `playing` attributes set the matching properties. Properties never write back to attributes.

## Events

All events bubble and are composed.

| Event | Detail | When |
| --- | --- | --- |
| `braccato:lyrics-loaded` | { lineCount, syncType } | Lyrics were applied, including an empty array |
| `braccato:line-click` | { timeS } | A line was clicked. With a bound source, the seek has already happened |
| `braccato:scroll-state` | { userScrolling } | Autoscroll stopped following the song, or started again |
| `braccato:error` | { phase, error } | Something failed while connecting, resolving the source, or applying lyrics or a theme |

Errors are dispatched one microtask after they happen, so a listener added in the same task that inserts the element still receives them. If you attach listeners later, read `status` instead.

## Theming

A theme is a CSS string written to `theme`. Settings that change behavior go in comments with a `blyrics-` prefix. Everything else is ordinary CSS.

```javascript
view.theme = `
  /* blyrics-target-scroll-pos-ratio = 0.5; */
  /* blyrics-long-word-threshold = 900; */

  .blyrics-container {
    --blyrics-font-size: 3rem;
    --blyrics-lyric-active-color: white;
    --blyrics-lyric-inactive-color: rgb(255 255 255 / 0.3);
  }
`;
```

`variables.css` declares every `--blyrics-*` custom property. The class names (`blyrics-container`, `blyrics--line`, `blyrics--active`, `blyrics--word` and others) are public API. Import them from `@braccato/core/constants` instead of typing them. Each word also carries `data-word-state` (`upcoming`, `active` or `past`).

If you write themes in RICS, compile them to CSS with `@braccato/rics` first.

## React example

The element is a normal custom element, so in React you set its properties through a ref. This example drives the clock from the player and seeks on line click.

```javascript
import "@braccato/core/element";
import "@braccato/core/styles/variables.css";
import "@braccato/core/styles/lyrics.css";
import "@braccato/core/styles/instrumental.css";
import { useEffect, useRef } from "react";

export function Lyrics({ lyrics, getCurrentTime, getPlaying, onSeek }) {
  const ref = useRef(null);

  useEffect(() => {
    ref.current.lyrics = lyrics;
  }, [lyrics]);

  useEffect(() => {
    let frame;
    const tick = () => {
      ref.current.currentTime = getCurrentTime();
      ref.current.playing = getPlaying();
      frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [getCurrentTime, getPlaying]);

  useEffect(() => {
    const el = ref.current;
    const seek = (event) => onSeek(event.detail.timeS);
    const noteUserScroll = () => el.renderer?.noteUserScroll();
    el.addEventListener("braccato:line-click", seek);
    el.addEventListener("scroll", noteUserScroll, { passive: true });
    return () => {
      el.removeEventListener("braccato:line-click", seek);
      el.removeEventListener("scroll", noteUserScroll);
    };
  }, [onSeek]);

  return <braccato-lyrics ref={ref} style={{ display: "block", height: 600, overflowY: "auto" }} />;
}
```

## Limits

-   One renderer per document. The theme and scroll padding are written per document, so two elements given different themes both report `theme-conflict`. Both keep rendering.
-   Load one copy of the package per page. The first copy to load registers the element names.
-   A browser extension's isolated world has no custom element registry. Use the element from the page's world, or call `createLyricsRenderer` from `@braccato/core` directly.

Source: https://docs.betterlyrics.org/braccato/renderer
