Building a Basic React Music Player with Advanced Design

Building a Basic React Music Player with Advanced Design
2024-11-07•Basic•4 min

In this blog, we'll guide you step-by-step in creating a basic music player in React. Whether you're just starting with React or looking to expand your skills, this project will give you a hands-on understanding of state management, component creation, and how to design an intuitive and visually appealing music player.

Why Build a Music Player?

Creating a music player is an excellent beginner project that helps you understand the core concepts of React, including

  • State management: Handling the current song, volume, and playback state.
  • Event handling: Responding to user inputs such as play, pause, and volume adjustments.
  • UI design: Building a sleek and responsive interface.
  • Let’s get started!

    Setting Up Your React Project

    npx create-react-app react-music-player

    Installing Necessary Dependencies

    npm install framer-motion
    npm install lucide-react

    Now that our project is set up, we can start building the music player.

    Before that, Let's Take a look what we are going to create in this project

    Building a Basic React Music Player with Advanced Design
    Building a Basic React Music Player with Advanced Design
    Building a Basic React Music Player with Advanced Design

    Here’s the code for our Main.jsx:

    'use client'
    
    import { useState, useEffect, useRef } from 'react'
    import { Search, SkipBack, Play, Pause, SkipForward, Volume2, Shuffle, Repeat, Radio, Library, Menu, X } from 'lucide-react'
    import { motion, AnimatePresence } from 'framer-motion'
    import song from './Bloody_Mary.mp3'
    
    export default function Component() {
      const [currentSongIndex, setCurrentSongIndex] = useState(0)
      const [isPlaying, setIsPlaying] = useState(false)
      const [volume, setVolume] = useState(1)
      const [currentTime, setCurrentTime] = useState(0)
      const [duration, setDuration] = useState(0)
      const [isShuffling, setIsShuffling] = useState(false)
      const [isLooping, setIsLooping] = useState(false)
      const [activeTab, setActiveTab] = useState('DISCOVER')
      const [isSidebarOpen, setIsSidebarOpen] = useState(true)
    
    
      const audioRef = useRef(new Audio())
    
      const playlistItems = [
        { id: '01', img: "https://upload.wikimedia.org/wikipedia/en/3/35/Flume_-_Skin.png", title: 'Skin', artist: 'Flume', src: song },
        { id: '02', img: "https://upload.wikimedia.org/wikipedia/en/2/28/Imagine_Dragons_Thunder.jpg", title: 'Thunder', artist: 'Imagine Dragons', src: song },
        { id: '03', img: "https://upload.wikimedia.org/wikipedia/en/9/90/Jumpsuit-Nico_and_the_Niners_single_cover.png", title: 'Jumpsuit', artist: 'Twenty One Pilots', src: song },
        { id: '04', img: "https://upload.wikimedia.org/wikipedia/en/3/35/Flume_-_Skin.png", title: 'Side Piece', artist: 'Snoop Dogg', src: song },
        { id: '05', img: "https://i1.sndcdn.com/artworks-JrMt6yEvPiG1-0-t500x500.jpg", title: 'Surrender', artist: 'Hurts', src: song }
    ]
    
      const popularAlbums = [
        { title: 'Children of Bodom', artist: 'Hexed', image: 'https://i.scdn.co/image/ab6761610000e5ebe51866b7c973c8aafaf1fece' },
        { title: 'Black Water', artist: 'MARUV', image: 'https://is1-ssl.mzstatic.com/image/thumb/Music128/v4/d0/30/ab/d030abac-f46c-ffd3-89a3-957c1125f47c/190295594541.jpg/1200x1200bb.jpg' },
        { title: 'Hurts 2B Human', artist: 'Pink', image: 'https://upload.wikimedia.org/wikipedia/en/1/17/Pink_-_Hurts_2B_Human.png' },
        { title: 'No Sound Without', artist: 'The Script', image: 'https://upload.wikimedia.org/wikipedia/en/3/39/No_Sound_Without_Silence_by_The_Script.png' }
      ]
    
      const popularArtists = [
        { name: 'Lady Gaga', image: 'https://variety.com/wp-content/uploads/2024/09/gaga_4d20fb.png?w=1000&h=667&crop=1' },
        { name: 'Bruno Mars', image: 'https://hips.hearstapps.com/hmg-prod/images/gettyimages-134315104.jpg' },
        { name: 'Beyonce', image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/ca/Beyonc%C3%A9_-_Tottenham_Hotspur_Stadium_-_1st_June_2023_%285_of_118%29_%2852945900006%29_%28cropped_2%29.jpg/640px-Beyonc%C3%A9_-_Tottenham_Hotspur_Stadium_-_1st_June_2023_%285_of_118%29_%2852945900006%29_%28cropped_2%29.jpg' },
        { name: 'Dua Lipa', image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Glasto24_28_300624_%28259_of_545%29_%2853838014719%29_%28cropped%29.jpg/640px-Glasto24_28_300624_%28259_of_545%29_%2853838014719%29_%28cropped%29.jpg' },
        { name: 'MARUV', image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUbquH_Ya0d5KQqVMBEX11-piTdY_pd2wLpA&s' }
      ]
    
      const libraryAlbums = [
        { title: 'Evolve', artist: 'Imagine Dragons', image: 'https://upload.wikimedia.org/wikipedia/en/1/11/Evolve_Box_Art.png' },
        { title: 'Blurryface', artist: 'Twenty One Pilots', image: 'https://is1-ssl.mzstatic.com/image/thumb/Music211/v4/8e/e2/89/8ee28904-0821-610d-5011-a61845f62756/075679926951.jpg/1200x1200bf-60.jpg' },
        { title: 'Chromatica', artist: 'Lady Gaga', image: 'https://upload.wikimedia.org/wikipedia/en/d/d6/Lady_Gaga_-_Chromatica_%28Official_Album_Cover%29.png' },
        { title: 'Future Nostalgia', artist: 'Dua Lipa', image: 'https://i.scdn.co/image/ab67616d0000b2734bc66095f8a70bc4e6593f4f' },
        { title: 'After Hours', artist: 'The Weeknd', image: 'https://i.scdn.co/image/ab67616d0000b2738863bc11d2aa12b54f5aeb36' },
        { title: 'Lover', artist: 'Taylor Swift', image: 'https://upload.wikimedia.org/wikipedia/en/c/cd/Taylor_Swift_-_Lover.png' },
      ]
    
      const radioStations = [
        { name: 'Pop Hits', frequency: '98.5 FM', image: 'https://i1.sndcdn.com/artworks-000238705219-wqv8dm-t500x500.jpg' },
        { name: 'Rock Classics', frequency: '101.1 FM', image: 'https://resources.tidal.com/images/64fc71d0/70f8/414d/ba14/30059e01f14c/640x640.jpg' },
        { name: 'Smooth Jazz', frequency: '103.7 FM', image: 'https://cdn-profiles.tunein.com/s249973/images/logog.jpg?t=162282' },
        { name: 'EDM Beats', frequency: '95.3 FM', image: 'https://i.scdn.co/image/ab67616d0000b2734eb60efea889b97c0ee6b171' },
        { name: 'Hip Hop Now', frequency: '106.2 FM', image: 'https://www.storybench.org/wp-content/uploads/2018/12/Rap-Bubbles-e1544644776922.png' },
      ]
    
      useEffect(() => {
        const audio = audioRef.current
        audio.src = playlistItems[currentSongIndex].src
        audio.volume = volume
    
        const updateTime = () => setCurrentTime(audio.currentTime)
        const updateDuration = () => setDuration(audio.duration)
    
        audio.addEventListener('timeupdate', updateTime)
        audio.addEventListener('loadedmetadata', updateDuration)
        audio.addEventListener('ended', handleNext)
    
        return () => {
          audio.removeEventListener('timeupdate', updateTime)
          audio.removeEventListener('loadedmetadata', updateDuration)
          audio.removeEventListener('ended', handleNext)
        }
      }, [currentSongIndex])
    
      useEffect(() => {
        const audio = audioRef.current;
        
        // Set the new song source when the index changes
        audio.src = playlistItems[currentSongIndex].src;
        
        // Wait until the metadata is loaded before playing
        const onLoadedMetadata = () => {
          if (isPlaying) {
            audio.play(); // Automatically play when the song is set
          }
        };
      
        audio.addEventListener('loadedmetadata', onLoadedMetadata);
      
        // Clean up the event listener
        return () => {
          audio.removeEventListener('loadedmetadata', onLoadedMetadata);
        };
      }, [currentSongIndex, isPlaying]); 
      
      // Handle toggling play/pause
      const togglePlay = () => {
        const audio = audioRef.current;
        
        if (isPlaying) {
          audio.pause(); // Pause the song
        } else {
          audio.play(); // Play the song
        }
        setIsPlaying(!isPlaying); // Toggle the play state
      };
      
      const handleNext = () => {
        setCurrentSongIndex((prevIndex) => {
          if (isShuffling) {
            let newIndex
            do {
              newIndex = Math.floor(Math.random() * playlistItems.length)
            } while (newIndex === prevIndex)
            return newIndex
          }
          return (prevIndex + 1) % playlistItems.length
        })
      }
    
      const handlePrevious = () => {
        setCurrentSongIndex((prevIndex) => {
          if (isShuffling) {
            let newIndex
            do {
              newIndex = Math.floor(Math.random() * playlistItems.length)
            } while (newIndex === prevIndex)
            return newIndex
          }
          return prevIndex === 0 ? playlistItems.length - 1 : prevIndex - 1
        })
      }
    
      const handleVolumeChange = (e) => {
        const newVolume = parseFloat(e.target.value)
        setVolume(newVolume)
        audioRef.current.volume = newVolume
      }
    
      const handleSeek = (e) => {
        const seekTime = parseFloat(e.target.value)
        setCurrentTime(seekTime)
        audioRef.current.currentTime = seekTime
      }
    
      const toggleShuffle = () => setIsShuffling(!isShuffling)
      const toggleLoop = () => {
        setIsLooping(!isLooping)
        audioRef.current.loop = !isLooping
      }
    
      const formatTime = (time) => {
        const minutes = Math.floor(time / 60)
        const seconds = Math.floor(time % 60)
        return `${minutes}:${seconds.toString().padStart(2, '0')}`
      }
    
      const renderDiscoverContent = () => (
        <>
          {/* Hero Section */}
          <motion.div 
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            className="mb-8 md:mb-16 relative overflow-hidden rounded-2xl"
          >
            <div className="absolute inset-0 bg-gradient-to-r from-teal-900/90 to-transparent z-10"></div>
            <img 
              src="https://upload.wikimedia.org/wikipedia/en/3/35/Flume_-_Skin.png" 
              alt="Hero" 
              className="w-full h-[200px] md:h-[400px] object-cover"
            />
            <div className="absolute bottom-4 md:bottom-12 left-4 md:left-12 z-20">
              <h1 className="text-3xl md:text-6xl font-bold mb-2 md:mb-4">Flume: Skin</h1>
              <p className="text-sm md:text-lg text-gray-200 mb-4 md:mb-8 max-w-xl hidden md:block">
                Flume walks us through his 'weird' new album, 'Skin'.
                He has one goal: "I wanna make weird stuff."
              </p>
              <div className="flex space-x-4">
                <button 
                  className="bg-teal-400 text-black px-4 md:px-8 py-2 md:py-3 rounded-full hover:bg-teal-300 transition-colors text-sm md:text-base"
                  onClick={() => {
                    setCurrentSongIndex(0)
                    setIsPlaying(true)
                    audioRef.current.play()
                  }}
                >
                  PLAY
                </button>
                <button className="border border-white px-4 md:px-8 py-2 md:py-3 rounded-full hover:bg-white/10 transition-colors text-sm md:text-base">
                  FOLLOW
                </button>
              </div>
            </div>
          </motion.div>
    
          {/* Popular Albums */}
          <section className="mb-8 md:mb-16">
            <h2 className="text-xl md:text-2xl font-bold mb-4 md:mb-8">Popular Albums</h2>
            <div className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
              {popularAlbums.map((album, index) => (
                <motion.div 
                  key={album.title}
                  initial={{ opacity: 0, y: 20 }}
                  animate={{ opacity: 1, y: 0 }}
                  transition={{ delay: index * 0.1 }}
                  className="group cursor-pointer"
                >
                  <div className="relative overflow-hidden rounded-lg mb-2 md:mb-4">
                    <img 
                      src={album.image} 
                      alt={album.title} 
                      className="w-full aspect-square object-cover transform group-hover:scale-105 transition-transform duration-300"
                    />
                    <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
                      <Play className="w-8 md:w-12 h-8 md:h-12" />
                    </div>
                  </div>
                  <h3 className="font-medium text-sm md:text-base">{album.title}</h3>
                  <p className="text-xs md:text-sm text-gray-400">{album.artist}</p>
                </motion.div>
              ))}
            </div>
          </section>
    
          {/* Popular Artists */}
          <section>
            <h2 className="text-xl md:text-2xl font-bold mb-4 md:mb-8">Popular Artists</h2>
            <div className="flex space-x-4 md:space-x-8 overflow-x-auto pb-4">
              {popularArtists.map((artist, index) => (
                <motion.div 
                  key={artist.name}
                  initial={{ opacity: 0, scale: 0.8 }}
                  animate={{ opacity: 1, scale: 1 }}
                  transition={{ delay: index * 0.1 }}
                  className="text-center flex-shrink-0"
                >
                  <div className="w-16 h-16 md:w-20 md:h-20 rounded-full overflow-hidden mb-2 md:mb-3 mx-auto">
                    <img 
                      src={artist.image} 
                      alt={artist.name} 
                      className="w-full h-full object-cover hover:scale-110 transition-transform duration-300"
                    />
                  </div>
                  <p className="text-xs md:text-sm">{artist.name}</p>
                </motion.div>
              ))}
            </div>
          </section>
        </>
      )
    
      const renderLibraryContent = () => (
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          className="mb-8 md:mb-16"
        >
          <h2 className="text-2xl md:text-3xl font-bold mb-4 md:mb-8">My Library</h2>
          <div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
            {libraryAlbums.map((album, index) => (
              <motion.div
                key={album.title}
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: index * 0.1 }}
                className="bg-white/5 p-3 md:p-4 rounded-lg hover:bg-white/10 transition-colors"
              >
                <img
                  src={album.image}
                  alt={album.title}
                  className="w-full aspect-square object-cover rounded-lg mb-2 md:mb-4"
                />
                <h3 className="font-semibold text-sm md:text-lg">{album.title}</h3>
                <p className="text-xs md:text-sm text-gray-400">{album.artist}</p>
              </motion.div>
            ))}
          </div>
        </motion.div>
      )
    
      const renderRadioContent = () => (
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          className="mb-8 md:mb-16"
        >
          <h2 className="text-2xl md:text-3xl font-bold mb-4 md:mb-8">Radio Stations</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
            {radioStations.map((station, index) => (
              <motion.div
                key={station.name}
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: index * 0.1 }}
                className="bg-white/5 p-3 md:p-4 rounded-lg flex items-center space-x-3 md:space-x-4 hover:bg-white/10 transition-colors"
              >
                <img
                  src={station.image}
                  alt={station.name}
                  className="w-12 h-12 md:w-16 md:h-16 rounded-full"
                />
                <div>
                  <h3 className="font-semibold text-sm md:text-lg">{station.name}</h3>
                  <p className="text-xs md:text-sm text-gray-400">{station.frequency}</p>
                </div>
                <button className="ml-auto bg-teal-400 text-black p-2 rounded-full hover:bg-teal-300 transition-colors">
                  <Radio className="w-4 h-4 md:w-6 md:h-6" />
                </button>
              </motion.div>
            ))}
          </div>
        </motion.div>
      )
    
      return (
        <div className="h-screen bg-gradient-to-br from-teal-900 via-slate-900 to-slate-950 text-white overflow-hidden flex flex-col">
          {/* Main Content */}
          <div className="flex flex-1 overflow-hidden">
            {/* Sidebar */}
            <AnimatePresence>
              {isSidebarOpen && (
                <motion.div 
                  initial={{ x: -300 }}
                  animate={{ x: 0 }}
                  exit={{ x: -300 }}
                  className="fixed inset-y-0 left-0 w-72 bg-slate-950/90 p-6 overflow-y-auto z-50 md:relative md:block"
                >
                  <button 
                    className="absolute top-4 right-4 md:hidden"
                    onClick={() => setIsSidebarOpen(false)}
                  >
                    <X className="w-6 h-6" />
                  </button>
                  <h1 className="text-2xl font-bold mb-8">Playlist</h1>
                  <nav className="space-y-4 mb-8">
                   {
                    /*
                    <a href="#" className="flex items-center space-x-2 text-teal-400">
                      <span className="w-1 h-1 bg-teal-400 rounded-full"></span>
                      <span>My Playlist</span>
                    </a> 
                    */
                   }
                    <a href="#" className="flex items-center space-x-2 text-gray-400 hover:text-white transition-colors">
                      <span>Recommended</span>
                    </a>
                  </nav>
                  <div className="space-y-4">
                    {playlistItems.map((item, index) => (
                      <motion.div 
                        key={item.id}
                        whileHover={{ scale: 1.02 }}
                        className={`flex items-center space-x-3 group cursor-pointer ${index === currentSongIndex ? 'text-teal-400' : ''}`}
                        onClick={() => {
                          setCurrentSongIndex(index)
                          setIsPlaying(true)
                          audioRef.current.play()
                        }}
                      >
                        <span className="text-sm text-gray-500">{item.id}</span>
                        <img src={`${item.img}`} alt={`${item.title}`} className="w-10 h-10 rounded" />
                        <div className="flex-1">
                          <p className="text-sm font-medium">{item.title}</p>
                          <p className="text-xs text-gray-400">{item.artist}</p>
                        </div>
                      </motion.div>
                    ))}
                  </div>
                </motion.div>
              )}
            </AnimatePresence>
    
            {/* Main Content Area */}
            <div className="flex-1 overflow-y-auto p-4 md:p-8">
              {/* Navigation */}
              <div className="flex items-center justify-between mb-6 md:mb-12">
                <button 
                  className="md:hidden"
                  onClick={() => setIsSidebarOpen(true)}
                >
                  <Menu className="w-6 h-6" />
                </button>
                <nav className="flex space-x-4 md:space-x-8">
                  {['DISCOVER', 'MY LIBRARY', 'RADIO'].map((tab) => (
                    <a
                      key={tab}
                      href="#"
                      className={`${
                        activeTab === tab ? 'text-teal-400 border-b-2 border-teal-400' : 'text-gray-400'
                      } hover:text-white transition-colors pb-2 flex items-center space-x-1 md:space-x-2 text-sm md:text-base`}
                      onClick={() => setActiveTab(tab)}
                    >
                      {tab === 'DISCOVER' && <Search className="w-3 h-3 md:w-4 md:h-4" />}
                      {tab === 'MY LIBRARY' && <Library className="w-3 h-3 md:w-4 md:h-4" />}
                      {tab === 'RADIO' && <Radio className="w-3 h-3 md:w-4 md:h-4" />}
                      <span>{tab}</span>
                    </a>
                  ))}
                </nav>
                <div className="relative hidden md:block">
                  <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
                  <input 
                    type="text" 
                    placeholder="Search artist, albums..."
                    className="bg-white/10 rounded-full pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-teal-400 w-64"
                  />
                </div>
              </div>
    
              {/* Conditional Content Rendering */}
              {activeTab === 'DISCOVER' && renderDiscoverContent()}
              {activeTab === 'MY LIBRARY' && renderLibraryContent()}
              {activeTab === 'RADIO' && renderRadioContent()}
            </div>
          </div>
    
          {/* Player Bar */}
          <motion.div 
            initial={{ y: 100 }}
            animate={{ y: 0 }}
            className="h-20 bg-slate-950/90 border-t border-white/10 flex items-center px-4"
          >
            <div className="flex items-center flex-1">
              <img 
                src={`${playlistItems[currentSongIndex].img}`} 
                alt={`${playlistItems[currentSongIndex].title}`}
                className="w-10 h-10 md:w-12 md:h-12 rounded mr-2 md:mr-4" 
              />
              <div className="hidden md:block">
                <p className="font-medium text-sm md:text-base">{playlistItems[currentSongIndex].title}</p>
                <p className="text-xs md:text-sm text-gray-400">{playlistItems[currentSongIndex].artist}</p>
              </div>
            </div>
            <div className="flex-1 flex flex-col items-center">
              <div className="flex items-center space-x-2 md:space-x-6">
                <button 
                  className={`text-gray-400 hover:text-white transition-colors ${isShuffling ? 'text-teal-400' : ''}`}
                  onClick={toggleShuffle}
                >
                  <Shuffle className="w-4 h-4 md:w-5 md:h-5" />
                </button>
                <button className="text-gray-400 hover:text-white transition-colors" onClick={handlePrevious}>
                  <SkipBack className="w-4 h-4 md:w-5 md:h-5" />
                </button>
                <button 
                  className="w-8 h-8 rounded-full bg-white text-black flex items-center justify-center hover:bg-gray-200 transition-colors"
                  onClick={togglePlay}
                >
                  {isPlaying ? <Pause className="w-4 h-4 md:w-5 md:h-5" /> : <Play className="w-4 h-4 md:w-5 md:h-5" />}
                </button>
                <button className="text-gray-400 hover:text-white transition-colors" onClick={handleNext}>
                  <SkipForward className="w-4 h-4 md:w-5 md:h-5" />
                </button>
                <button 
                  className={`text-gray-400 hover:text-white transition-colors ${isLooping ? 'text-teal-400' : ''}`}
                  onClick={toggleLoop}
                >
                  <Repeat className="w-4 h-4 md:w-5 md:h-5" />
                </button>
              </div>
              <div className="w-full flex items-center space-x-2 md:space-x-4 mt-1 md:mt-2">
                <span className="text-xs text-gray-400 hidden md:inline">{formatTime(currentTime)}</span>
                <input
                  type="range"
                  min="0"
                  max={duration || 0}
                  value={currentTime}
                  onChange={handleSeek}
                  className="flex-1 h-1 bg-white/10 rounded-full appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2 [&::-webkit-slider-thumb]:h-2 md:[&::-webkit-slider-thumb]:w-3 md:[&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
                />
                <span className="text-xs text-gray-400 hidden md:inline">{formatTime(duration)}</span>
              </div>
            </div>
            <div className="flex-1 hidden md:flex justify-end items-center space-x-4">
              <Volume2 className="w-5 h-5 text-gray-400" />
              <input
                type="range"
                min="0"
                max="1"
                step="0.01"
                value={volume}
                onChange={handleVolumeChange}
                className="w-24 h-1 bg-white/10 rounded-full appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
              />
            </div>
          </motion.div>
        </div>
      )
    }