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
has the full reference.
Install
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.
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:
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:
.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:
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.
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:
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.
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.
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.
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.
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
createLyricsRendererfrom@braccato/coredirectly.