4.0 KiB
karaoke/lyrics-display
(no submodules)
Purpose
<LyricsDisplay> is the karaoke surface that renders a stack of
lyric lines with the active line highlighted. It is the visible
end-product of the parseLrc → useLyrics pipeline and the
component the player variants compose when their lrc prop is
populated.
The component is clock-agnostic by design: the caller passes
currentSec (a plain number) and the component renders the
highlighted line. The typical wiring is useTransportTime() from
@suaveplan/react-audio when the display sits inside an
<AudioProvider>, but standalone preview and test surfaces can
hand in any number — Playwright spec runs use a deterministic
clock; documentation pages drive a <range> input. Keeping the time
source explicit removes a hidden provider dependency.
Features
- Renders one
<span>per lyric line withdata-active="true|false"for CSS-driven styling; consumers needing structural control can passrenderLinefor full custom rendering. - Active line carries
aria-live="polite"by default so screen readers announce lyric changes; togglable viaannounceLyrics. - Wrapping
<div>exposesrole="region"+aria-label="Lyrics". - Re-binds the active index every 100 ms (configurable) regardless of the consumer's render cadence.
- Both time-source modes share the same internal render path; the outer dispatch is a one-time component-type pick.
Usage: Basic
import { LyricsDisplay } from "@suaveplan/react-audio-player/karaoke/lyrics-display";
import { useTransportTime } from "@suaveplan/react-audio";
function Player({ lrcSource }: { lrcSource: string }) {
const seconds = useTransportTime();
return <LyricsDisplay lrc={lrcSource} currentSec={seconds} />;
}
Usage: Advanced
import { LyricsDisplay } from "@suaveplan/react-audio-player/karaoke/lyrics-display";
import { parseLrc } from "@suaveplan/react-audio-player/karaoke/lrc-parser";
import { useMemo } from "react";
function StyledLyrics({ lrc, time }: { lrc: string; time: number }) {
const lyrics = useMemo(() => parseLrc(lrc), [lrc]);
return (
<LyricsDisplay
lyrics={lyrics}
currentSec={time}
intervalMs={50}
announceLyrics={false}
renderLine={({ lyric, isActive }) => (
<p
style={{
opacity: isActive ? 1 : 0.4,
fontWeight: isActive ? 700 : 400,
}}
>
{lyric.text || <span aria-hidden="true">·</span>}
</p>
)}
/>
);
}
API Reference
<LyricsDisplay>acceptsLyricsDisplayProps:lyrics?: readonly Lyric[]— preferred overlrc.lrc?: string— parsed once per identity change.currentSec: number— required.intervalMs?: number(default 100).announceLyrics?: boolean(defaulttrue).className?: string.renderLine?: ({ lyric, isActive, index }) => ReactNode.
Implementation Notes
The component is a single-branch render — no dual-component
dispatch — because currentSec is always a plain prop. This keeps
the Rules of Hooks trivially satisfied (one render path, one hook
sequence) and removes a hidden coupling to the surrounding
<AudioProvider>. Consumers that want the transport-driven path
call useTransportTime() themselves and pass the result down.
The data-active="true|false" attribute is the recommended CSS
selector hook. Toggling aria-live only on the active line ensures
exactly one element narrates per change cycle; without that targeted
gating, every line's render would re-announce on every re-mount,
which is acoustically unusable.
When renderLine is supplied, the component delegates to it
verbatim and skips the default <span> + data-active path. The
delegated node is wrapped in a keyed <Fragment> so consumers can
return any element they like — <li>, <p>, <div> — without
fighting an enclosing wrapper.