Files
skills/docs/ui/react/react-audio-player/src/karaoke/lyrics-display/lyrics-display.md
2026-05-29 08:20:56 +01:00

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 with data-active="true|false" for CSS-driven styling; consumers needing structural control can pass renderLine for full custom rendering.
  • Active line carries aria-live="polite" by default so screen readers announce lyric changes; togglable via announceLyrics.
  • Wrapping <div> exposes role="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> accepts LyricsDisplayProps:
    • lyrics?: readonly Lyric[] — preferred over lrc.
    • lrc?: string — parsed once per identity change.
    • currentSec: number — required.
    • intervalMs?: number (default 100).
    • announceLyrics?: boolean (default true).
    • 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.