Running on Monochrome in fullscreen.
This webcomponent follows the open-wc recommendation.
This web component is my take on Apple Music's word by word lyrics, utilises various APIs to fetch lyrics and the animations are inspired by YouLy+, Apple Music-like Lyrics and LyricsBlossom's Swift reference. It's designed to be faithful while also being lightweight and able to run on all major browsers and many mid-to-high end devices smoothly. Because it's a Web Component, it's also really easy to use!
npm install @uimaxbai/am-lyrics # Or your package manager of choiceOr just use CDN (below).
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics/dist/src/am-lyrics.min.js';
</script>
<am-lyrics
song-title="Uptown Funk"
song-artist="Mark Ronson"
song-album="Uptown Special"
song-duration="269000"
query="Uptown Funk Mark Ronson"
current-time="0"
duration=""
highlight-color="#f00"
font-family="'Inter', Arial, sans-serif"
autoscroll
interpolate
></am-lyrics>| Property/Attribute | Type | Default | Description |
|---|---|---|---|
query |
string |
undefined |
Search phrase for lrc.red, with LyricsPlus catalog fallback |
music-id |
string |
undefined |
Specific Apple Music song ID (served through the backup Apple endpoint) |
spotify-id |
string |
undefined |
Spotify track ID for Spicy Lyrics; skips the Spotify search when supplied |
isrc |
string |
undefined |
ISRC code for direct lrc.red TTML lookup and provider matching |
ttml |
string |
undefined |
A string of TTML formatted lyrics to be rendered directly (bypasses all external requests) |
song-title |
string |
undefined |
Track title for lrc.red and fallback providers |
song-artist |
string |
undefined |
Artist name for lrc.red and fallback providers |
song-album |
string |
undefined |
Optional album name for provider matching |
song-duration |
number |
undefined |
Optional song duration in milliseconds sent to LyricsPlus |
current-time |
number |
0 |
Current playback time in milliseconds |
duration |
number |
undefined |
Playback timer duration in milliseconds. Set to -1 to reset/stop playback |
highlight-color |
string |
"#fff" |
Color for highlighted/active lyrics |
hide-source-footer |
boolean |
false |
Hide/show the source attribution footer |
font-family |
string |
undefined |
Custom font family for lyrics |
autoscroll |
boolean |
true |
Enable automatic scrolling to active lyrics |
interpolate |
boolean |
true |
Enable smooth word-by-word highlighting animation |
You can customize the appearance using CSS custom properties:
am-lyrics {
/* Highlight color for active lyrics */
--am-lyrics-highlight-color: #007aff;
/* Alternative highlight color (fallback) */
--highlight-color: #000;
/* Swift-derived layout and motion defaults */
--lyplus-font-size-base: 34px;
--am-lyrics-line-height: 1.2;
--am-lyrics-line-spacing: 25px;
--am-lyrics-background-vocal-spacing: 15px;
--am-lyrics-background-vocal-font-size: 0.65em;
--am-lyrics-background-vocal-stack-shift: 7.5px;
--am-lyrics-background-vocal-max-height: 8em;
--am-lyrics-background-vocal-exit-duration: 450ms;
--am-lyrics-instrumental-height: 40px;
--am-lyrics-instrumental-spacing: 16px;
--am-lyrics-instrumental-enter-duration: 400ms;
--am-lyrics-instrumental-collapse-duration: 500ms;
--am-lyrics-instrumental-exit-duration: 350ms;
--am-lyrics-instrumental-exit-scale: 0;
--am-lyrics-inactive-scale: 0.98;
--am-lyrics-background-vocal-scale: 0.9;
--am-lyrics-touch-scale: 0.96;
--am-lyrics-highlight-radius: 16px;
--am-lyrics-highlight-surface: rgba(255, 255, 255, 0.08);
--am-lyrics-progression-feather: 30px;
--am-lyrics-glow-radius: 5px;
--am-lyrics-character-rise-peak: -1.25px;
--am-lyrics-inline-padding: 20px;
--lyrics-scroll-padding-top: 12%;
--am-lyrics-compact-font-size: 28px;
--am-lyrics-compact-background-vocal-font-size: 0.857em;
--am-lyrics-compact-line-spacing: 20px;
--am-lyrics-compact-selected-position: 18%;
--am-lyrics-wide-font-size: 48px;
--am-lyrics-wide-background-vocal-font-size: 0.667em;
--am-lyrics-wide-line-spacing: 32px;
--am-lyrics-wide-selected-position: 20%;
}Thank you to these providers who generously allow me to use their lyrics in my component:
lrc.red, BiniLyrics, Spicy Lyrics, Unison, LRCLib, Genius
And also thank you to anyone I might have missed!
Fired when a user clicks on a lyrics line.
amLyrics.addEventListener('line-click', event => {
console.log('Seek to:', event.detail.timestamp); // timestamp in milliseconds
});First, ensure you have react and @lit/react installed in your project.
npm install react @lit/react # Very important or errors will ariseThen, you can import the AmLyrics component from am-lyrics/react and use it in your components.
'use client'; // VERY IMPORTANT!!!
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { AmLyrics } from '@uimaxbai/am-lyrics/react';
export default function App() {
const [currentTime, setCurrentTime] = useState(0);
const audioRef = useRef<HTMLAudioElement>(null);
// Sync audio player time with the component
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
let animationFrameId: number;
const updateCurrentTime = () => {
setCurrentTime(audio.currentTime * 1000);
animationFrameId = requestAnimationFrame(updateCurrentTime); // Use requestAnimationFrame to prevent choppy scrolling
};
const handlePlay = () => {
animationFrameId = requestAnimationFrame(updateCurrentTime);
};
const handlePause = () => {
cancelAnimationFrame(animationFrameId);
};
const handleTimeUpdate = () => {
setCurrentTime(audio.currentTime * 1000); // Convert to milliseconds
};
audio.addEventListener('play', handlePlay);
audio.addEventListener('pause', handlePause);
audio.addEventListener('timeupdate', handleTimeUpdate);
return () => {
cancelAnimationFrame(animationFrameId);
audio.removeEventListener('play', handlePlay);
audio.removeEventListener('pause', handlePause);
audio.removeEventListener('timeupdate', handleTimeUpdate);
};
}, []);
// Handle line clicks to seek the audio
const handleLineClick = useCallback((event: Event) => {
const customEvent = event as CustomEvent<{ timestamp: number }>;
const audio = audioRef.current;
if (audio) {
audio.currentTime = customEvent.detail.timestamp / 1000; // Convert to seconds
audio.play();
}
}, []);
return (
<div>
<audio ref={audioRef} src="/uptown_funk.flac" controls />
<AmLyrics
songTitle="Uptown Funk"
songArtist="Mark Ronson"
query="Uptown Funk Mark Ronson"
currentTime={currentTime}
onLineClick={handleLineClick}
autoScroll
highlightColor='#fff'
/>
</div>
);
}Using NextJS? See next.md.
Lit web components only partially support SSR, so this package is very volatile in SSR. Either:
- Use the CDN solution and place it direct into your HTML or
- Disable SSR on the page with lyrics.
Using NextJS? See next.md.
The timer needs to be defined by yourself. For example:
<script>
let animationFrameId;
let songStartTime = 0;
let systemStartTime = 0;
function stopAnimation() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
function animate() {
const amLyrics = document.querySelector('am-lyrics');
if (!amLyrics) return;
const elapsedTime = Date.now() - systemStartTime;
amLyrics.currentTime = songStartTime + elapsedTime;
animationFrameId = requestAnimationFrame(animate);
}
function startPlayback() {
stopAnimation();
songStartTime = 0;
systemStartTime = Date.now(); // Use dates instead of setInterval() for more accuracy
animate();
}
function handleLineClick(e) {
stopAnimation();
songStartTime = e.detail.timestamp;
systemStartTime = Date.now();
animate();
}
function handleSearch() {
const searchInput = document.querySelector('#search-input');
const amLyrics = document.querySelector('am-lyrics');
if (searchInput && amLyrics) {
// Expect "Title - Artist" in the search field for optimal LyricsPlus results
const userInput = searchInput.value.trim();
const [titlePart = '', artistPart = ''] = userInput
.split(' - ')
.map(part => part.trim());
amLyrics.songTitle = titlePart || userInput;
amLyrics.songArtist = artistPart;
amLyrics.query = userInput;
amLyrics.isrc = '';
amLyrics.musicId = '';
}
}
document.addEventListener('DOMContentLoaded', () => {
const amLyrics = document.querySelector('am-lyrics');
const searchButton = document.querySelector('#search-button');
const startButton = document.querySelector('#start-button');
if (amLyrics) {
amLyrics.addEventListener('line-click', handleLineClick);
}
if (searchButton) {
searchButton.addEventListener('click', handleSearch);
}
if (startButton) {
startButton.addEventListener('click', startPlayback);
}
});
</script>You can synchronize the lyrics with an HTML <audio> element.
<audio id="audio-player" src="path/to/your/song.mp3" controls></audio>
<am-lyrics
song-title="Uptown Funk"
song-artist="Mark Ronson"
query="Uptown Funk Mark Ronson"
></am-lyrics>
<script>
document.addEventListener('DOMContentLoaded', () => {
const amLyrics = document.querySelector('am-lyrics');
const audioPlayer = document.querySelector('#audio-player');
if (amLyrics && audioPlayer) {
// Update lyrics time when audio time updates
audioPlayer.addEventListener('timeupdate', () => {
// The component expects time in milliseconds
amLyrics.currentTime = audioPlayer.currentTime * 1000;
});
// Seek audio when a lyric line is clicked
amLyrics.addEventListener('line-click', e => {
// The event detail contains the timestamp in milliseconds
audioPlayer.currentTime = e.detail.timestamp / 1000;
audioPlayer.play();
});
}
});
</script>See demo/index.html for a functional demo.
npm i
yarn install
bun inpm startTo scan the project for linting and formatting errors, run
npm run lintTo automatically fix linting and formatting errors, run
npm run format