Skip to main content

Overview

The LyricsContext provides a simple state management solution for controlling the visibility of the lyrics display panel. It’s a lightweight context that works alongside SpotifyContext to manage UI state.

LyricsProvider

Wraps your application to provide lyrics visibility state.

Props

children
ReactNode
required
The component tree that needs access to lyrics visibility state

Usage

import { LyricsProvider } from './contexts/LyricsContext';
import { SpotifyProvider } from './contexts/SpotifyContext';

function App() {
  return (
    <SpotifyProvider>
      <LyricsProvider>
        <YourApp />
      </LyricsProvider>
    </SpotifyProvider>
  );
}

useLyrics Hook

Access and control lyrics visibility state from any component within the provider.

Return Value

Returns a LyricsContextType object with the following properties:
lyricsVisible
boolean
Whether the lyrics display panel is currently visible
setLyricsVisible
(visible: boolean) => void
Function to update the lyrics visibility state

Usage Examples

Toggle Lyrics Button

import { useLyrics } from './contexts/LyricsContext';
import { useSpotify } from './contexts/SpotifyContext';

function LyricsToggle() {
  const { lyricsVisible, setLyricsVisible } = useLyrics();
  const { lyrics } = useSpotify();
  
  const hasLyrics = lyrics?.lines && lyrics.lines.length > 0;
  
  return (
    <button 
      disabled={!hasLyrics}
      onClick={() => setLyricsVisible(!lyricsVisible)}
    >
      {lyricsVisible ? 'Hide Lyrics' : 'Show Lyrics'}
    </button>
  );
}

Conditional Rendering

function LyricsPanel() {
  const { lyricsVisible } = useLyrics();
  const { lyrics, currentTrack } = useSpotify();
  
  if (!lyricsVisible || !lyrics) return null;
  
  return (
    <div className="lyrics-panel">
      <h2>{currentTrack?.name}</h2>
      {lyrics.lines.map((line, i) => (
        <p key={i}>{line.words}</p>
      ))}
    </div>
  );
}

Keyboard Shortcut

function LyricsKeyboardHandler() {
  const { lyricsVisible, setLyricsVisible } = useLyrics();
  
  useEffect(() => {
    const handleKeyPress = (e: KeyboardEvent) => {
      if (e.key.toLowerCase() === 'l') {
        setLyricsVisible(!lyricsVisible);
      }
    };
    
    window.addEventListener('keydown', handleKeyPress);
    return () => window.removeEventListener('keydown', handleKeyPress);
  }, [lyricsVisible, setLyricsVisible]);
  
  return null;
}

With View Transition API

function AnimatedLyricsToggle() {
  const { lyricsVisible, setLyricsVisible } = useLyrics();
  
  const handleToggle = () => {
    if (document.startViewTransition) {
      document.startViewTransition(() => {
        setLyricsVisible(!lyricsVisible);
      });
    } else {
      setLyricsVisible(!lyricsVisible);
    }
  };
  
  return (
    <button onClick={handleToggle}>
      Toggle Lyrics
    </button>
  );
}

Layout Adjustment

function AppLayout() {
  const { lyricsVisible } = useLyrics();
  
  const containerClass = lyricsVisible 
    ? "min-h-screen" 
    : "min-h-screen grid place-items-center";
  
  const albumArtClass = lyricsVisible
    ? "w-14 h-14 rounded-sm"
    : "w-80 h-80 rounded-lg";
  
  return (
    <div className={containerClass}>
      <img className={albumArtClass} src={albumArt} />
      {lyricsVisible && <LyricsDisplay />}
    </div>
  );
}

Sticky Header with Compact View

function NowPlayingHeader() {
  const { lyricsVisible } = useLyrics();
  const { currentTrack } = useSpotify();
  
  const headerClass = lyricsVisible
    ? "sticky top-0 p-3 grid grid-cols-[3.5rem_auto] gap-3"
    : "relative p-6 grid grid-rows-[20rem_auto]";
  
  return (
    <div className={headerClass}>
      <img 
        src={currentTrack?.album.images[0].url}
        className="aspect-square"
      />
      <div>
        <p>{currentTrack?.name}</p>
        <p>{currentTrack?.artists[0].name}</p>
      </div>
    </div>
  );
}

Auto-Hide on Track Change

function AutoHideLyrics() {
  const { setLyricsVisible } = useLyrics();
  const { currentTrack } = useSpotify();
  
  useEffect(() => {
    // Hide lyrics when track changes
    setLyricsVisible(false);
  }, [currentTrack?.id]);
  
  return null;
}

Scroll Lock

function LyricsWithScrollLock() {
  const { lyricsVisible } = useLyrics();
  
  useEffect(() => {
    if (lyricsVisible) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = 'auto';
    }
    
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, [lyricsVisible]);
  
  return <LyricsPanel />;
}

Real-World Implementation

From App.tsx (lines 88-93, 182-188):
function ImageFlipper({ track, prevTrack }) {
  const { lyricsVisible } = useLyrics();
  
  // Adjust container layout based on lyrics visibility
  const containerClasses = lyricsVisible
    ? "min-h-dvh"
    : "min-h-dvh grid place-items-center";
  
  const wrapperClasses = lyricsVisible
    ? "sticky top-0 p-3 z-20 grid grid-cols-[3.5rem_auto_auto]"
    : "relative size-80 grid grid-rows-[20rem_auto]";
  
  return (
    <div className={containerClasses}>
      <div className={wrapperClasses}>
        {/* Album art and track info */}
      </div>
      {lyricsVisible && <LyricsDisplay.Lyrics />}
    </div>
  );
}
From LyricsDisplay.tsx (lines 171-212):
const LyricsDisplayButton = () => {
  const { lyricsVisible, setLyricsVisible } = useLyrics();
  const { lyrics } = useSpotify();
  
  const handleClick = () => {
    if (document.startViewTransition) { 
      document.startViewTransition(() => {
        setLyricsVisible(!lyricsVisible);
      });
    } else {
      setLyricsVisible(!lyricsVisible);
    }
  };
  
  // Keyboard shortcut (L key)
  useEffect(() => {
    const handleKeyDown = ({ key }) => {
      if (key.toLowerCase() === "l") {
        handleClick();
      }
    };
    
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, []);
  
  const hasLyrics = lyrics?.lines && lyrics.lines.length > 0;
  
  return (
    <button 
      disabled={!hasLyrics}
      onClick={handleClick}
      className={hasLyrics ? "" : "opacity-50"}
    >
      {/* Lyrics icon */}
    </button>
  );
};

TypeScript Interface

interface LyricsContextType {
  lyricsVisible: boolean;
  setLyricsVisible: (visible: boolean) => void;
}

Best Practices

Always Check for Lyrics Availability

Before showing the lyrics panel, verify that lyrics are actually available:
const { lyrics } = useSpotify();
const { lyricsVisible, setLyricsVisible } = useLyrics();

const hasLyrics = lyrics?.lines && lyrics.lines.length > 0;

if (lyricsVisible && !hasLyrics) {
  setLyricsVisible(false);
}

Combine with Loading States

function LyricsButton() {
  const { lyrics } = useSpotify();
  const { setLyricsVisible } = useLyrics();
  const [isLoading, setIsLoading] = useState(false);
  
  const handleClick = async () => {
    if (!lyrics) {
      setIsLoading(true);
      // Wait for lyrics to load
      await new Promise(resolve => setTimeout(resolve, 1000));
      setIsLoading(false);
    }
    setLyricsVisible(true);
  };
  
  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? 'Loading...' : 'Show Lyrics'}
    </button>
  );
}

Responsive Design

Adjust UI based on both screen size and lyrics visibility:
function ResponsiveLyrics() {
  const { lyricsVisible } = useLyrics();
  const isMobile = useMediaQuery('(max-width: 768px)');
  
  if (lyricsVisible && isMobile) {
    return <FullScreenLyrics />;
  }
  
  if (lyricsVisible) {
    return <SidebarLyrics />;
  }
  
  return null;
}

Error Handling

The context will throw an error if used outside the provider:
export function useLyrics(): LyricsContextType {
  const context = useContext(LyricsContext);
  if (context === undefined) {
    throw new Error('useLyrics must be used within a LyricsProvider');
  }
  return context;
}
Always wrap your app with the provider:
// ✅ Correct
<LyricsProvider>
  <App />
</LyricsProvider>

// ❌ Wrong - will throw error
<App /> // useLyrics() called without provider

Build docs developers (and LLMs) love