Music Player
A customizable music player component with multiple themes, playback controls, progress tracking, and responsive design.
Installation Guide
1
Install Dependencies
Lucide React
npm install lucide-react
2
Copy Component Code
Music Player.tsx
TypeScript1"use client";
2
3import React, { useState, useEffect } from "react";
4import {
5 Play,
6 Pause,
7 SkipForward,
8 SkipBack,
9 Heart,
10 Repeat,
11 Shuffle,
12} from "lucide-react";
13import Image from "next/image";
14
15// Types
16export interface MusicPlayerProps {
17 theme?: "default" | "spotify" | "cosmic" | "nebula" | string;
18 shadow?: boolean;
19 rounded?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
20 artwork?: string;
21 trackTitle?: string;
22 artist?: string;
23 album?: string;
24 initialTime?: number;
25 totalDuration?: number;
26 className?: string;
27 autoPlay?: boolean;
28 onPlayPause?: (isPlaying: boolean) => void;
29 onTimeChange?: (time: number) => void;
30 onTrackEnd?: () => void;
31 controls?: {
32 shuffle?: boolean;
33 repeat?: boolean;
34 heart?: boolean;
35 };
36}
37
38export const MusicPlayer = ({
39 theme = "default",
40 shadow = true,
41 rounded = "xl",
42 artwork = "/api/placeholder/400/400",
43 trackTitle = "undefined",
44 artist = "undefined",
45 album = "undefined",
46 initialTime = 0,
47 totalDuration = 217,
48 className = "",
49 autoPlay = false,
50 onPlayPause,
51 onTimeChange,
52 onTrackEnd,
53 controls = {
54 shuffle: true,
55 repeat: true,
56 heart: true,
57 },
58}: MusicPlayerProps) => {
59 const [isPlaying, setIsPlaying] = useState(autoPlay);
60 const [currentTime, setCurrentTime] = useState(initialTime);
61 const [liked, setLiked] = useState(false);
62 const [mounted, setMounted] = useState(false);
63
64 useEffect(() => {
65 setMounted(true);
66 }, []);
67
68 useEffect(() => {
69 let interval: NodeJS.Timeout;
70
71 if (isPlaying) {
72 interval = setInterval(() => {
73 setCurrentTime((time) => {
74 if (time >= totalDuration) {
75 setIsPlaying(false);
76 if (onTrackEnd) onTrackEnd();
77 return 0;
78 }
79 const newTime = time + 1;
80 if (onTimeChange) onTimeChange(newTime);
81 return newTime;
82 });
83 }, 1000);
84 }
85
86 return () => {
87 if (interval) clearInterval(interval);
88 };
89 }, [isPlaying, totalDuration, onTimeChange, onTrackEnd]);
90
91 const formatTime = (seconds: number) => {
92 const min = Math.floor(seconds / 60);
93 const sec = seconds % 60;
94 return `${min}:${sec < 10 ? "0" + sec : sec}`;
95 };
96
97 const togglePlay = () => {
98 const newPlayState = !isPlaying;
99 setIsPlaying(newPlayState);
100 if (onPlayPause) onPlayPause(newPlayState);
101 };
102
103 const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
104 const progressBar = e.currentTarget;
105 const { left, width } = progressBar.getBoundingClientRect();
106 const clickPosition = e.clientX - left;
107 const percentage = clickPosition / width;
108 const newTime = Math.floor(totalDuration * percentage);
109
110 setCurrentTime(newTime);
111 if (onTimeChange) onTimeChange(newTime);
112 };
113
114 const toggleLike = () => {
115 setLiked(!liked);
116 };
117
118 const getThemeStyles = () => {
119 const baseStyles = "transition-all duration-300";
120 const shadowStyles = shadow ? "shadow-xl" : "";
121 const roundedStyles = getRoundedClass(rounded);
122
123 switch (theme) {
124 case "spotify":
125 return `
126 bg-white text-green-600 border-2 border-green-400
127 dark:bg-black dark:text-green-400 dark:border-green-400
128 ${shadowStyles} ${roundedStyles} ${baseStyles}
129 `
130 .replace(/\s+/g, " ")
131 .trim();
132
133 case "cosmic":
134 return `
135 bg-indigo-50 text-indigo-900 border border-indigo-200
136 dark:bg-gradient-to-br dark:from-indigo-950 dark:to-blue-950 dark:text-white dark:border-indigo-300
137 ${shadowStyles} ${roundedStyles} ${baseStyles}
138 `
139 .replace(/\s+/g, " ")
140 .trim();
141
142 case "nebula":
143 return `
144 bg-purple-100 text-purple-700 border border-purple-300
145 dark:bg-gradient-to-br dark:from-purple-900 dark:to-indigo-900
146 dark:text-purple-200 dark:border-purple-600
147 ${shadowStyles} ${roundedStyles} ${baseStyles}
148 `
149 .replace(/\s+/g, " ")
150 .trim();
151
152 default:
153 return `
154 bg-white text-zinc-900
155 dark:bg-zinc-900 dark:text-white
156 ${shadowStyles} ${roundedStyles} ${baseStyles}
157 `
158 .replace(/\s+/g, " ")
159 .trim();
160 }
161 };
162
163 const getRoundedClass = (size: string) => {
164 switch (size) {
165 case "sm":
166 return "rounded-sm";
167 case "md":
168 return "rounded-md";
169 case "lg":
170 return "rounded-lg";
171 case "xl":
172 return "rounded-xl";
173 case "2xl":
174 return "rounded-2xl";
175 case "full":
176 return "rounded-full";
177 default:
178 return "rounded-md";
179 }
180 };
181
182 if (!mounted) {
183 return null;
184 }
185
186 return (
187 <div className={`${getThemeStyles()} ${className} overflow-hidden`}>
188 <div className="w-full h-44 relative">
189 <Image
190 src={artwork}
191 alt={`${trackTitle} by ${artist}`}
192 width={400}
193 height={256}
194 className={`w-full h-full object-cover ${getRoundedClass(
195 rounded
196 ).replace("rounded", "rounded-t")}`}
197 />
198 <div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
199 </div>
200
201 <div className="p-6 flex flex-col">
202 <div className="flex items-start justify-between mb-6">
203 <div>
204 <h1 className="text-xl font-bold mb-1">{trackTitle}</h1>
205 <p className="text-sm opacity-70">
206 {artist} • {album}
207 </p>
208 </div>
209 {controls.heart && (
210 <button
211 onClick={toggleLike}
212 aria-label="Like"
213 className={`transition-colors hover:scale-105 ${
214 liked ? "text-red-500" : "text-gray-400 hover:text-white"
215 }`}
216 >
217 <Heart className={`h-6 w-6 ${liked ? "fill-current" : ""}`} />
218 </button>
219 )}
220 </div>
221
222 <div className="mb-2">
223 <div
224 className="relative h-1 bg-gray-700 rounded-full overflow-hidden cursor-pointer"
225 onClick={handleProgressClick}
226 >
227 <div
228 className={`absolute top-0 left-0 h-full ${
229 theme === "spotify"
230 ? "bg-green-500"
231 : theme === "cosmic"
232 ? "bg-indigo-500"
233 : theme === "nebula"
234 ? "bg-purple-500"
235 : "bg-white"
236 } rounded-full`}
237 style={{ width: `${(currentTime / totalDuration) * 100}%` }}
238 ></div>
239 </div>
240 <div className="flex justify-between mt-2">
241 <span className="text-xs opacity-70">
242 {formatTime(currentTime)}
243 </span>
244 <span className="text-xs opacity-70">
245 {formatTime(totalDuration)}
246 </span>
247 </div>
248 </div>
249
250 <div className="flex flex-col gap-5">
251 <div className="flex justify-between items-center">
252 {controls.shuffle && (
253 <button
254 aria-label="Shuffle"
255 className="opacity-70 hover:opacity-100 transition-opacity"
256 >
257 <Shuffle className="h-5 w-5" />
258 </button>
259 )}
260 <div className="flex items-center gap-5">
261 <button
262 aria-label="Skip Back"
263 className="opacity-70 hover:opacity-100 transition-opacity"
264 >
265 <SkipBack className="h-6 w-6" />
266 </button>
267 <button
268 aria-label="Play/Pause"
269 className={`${
270 theme === "spotify"
271 ? "bg-green-500"
272 : theme === "cosmic"
273 ? "bg-indigo-500"
274 : theme === "nebula"
275 ? "bg-purple-500"
276 : "bg-white"
277 } text-black rounded-full p-3 hover:scale-105 transition-transform`}
278 onClick={togglePlay}
279 >
280 {isPlaying ? (
281 <Pause className="h-6 w-6" />
282 ) : (
283 <Play className="h-6 w-6 fill-current" />
284 )}
285 </button>
286 <button
287 aria-label="Skip Forward"
288 className="opacity-70 hover:opacity-100 transition-opacity"
289 >
290 <SkipForward className="h-6 w-6" />
291 </button>
292 </div>
293 {controls.repeat && (
294 <button
295 aria-label="Repeat"
296 className="opacity-70 hover:opacity-100 transition-opacity"
297 >
298 <Repeat className="h-5 w-5" />
299 </button>
300 )}
301 </div>
302 </div>
303 </div>
304 </div>
305 );
306};
307
3
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
theme | string | "default" | Visual theme of the music player (default, spotify, cosmic, nebula, or custom). |
shadow | boolean | true | Whether to display shadow effect on the player. |
rounded | string | "xl" | Corner radius of the player component. |
artwork | string | "/placeholder.svg" | Path to the album artwork image. |
trackTitle | string | "undefined" | Title of the track being played. |
artist | string | "undefined" | Name of the artist or band. |
album | string | "After Hours" | Name of the album. |
initialTime | number | 0 | Initial playback position in seconds. |
totalDuration | number | 217 | Total duration of the track in seconds. |
className | string | "" | Additional CSS classes to apply to the component. |
autoPlay | boolean | false | Whether to start playback automatically. |
onPlayPause | function | undefined | Callback function triggered when play/pause is toggled. |
onTimeChange | function | undefined | Callback function triggered when playback time changes. |
onTrackEnd | function | undefined | Callback function triggered when track playback ends. |
controls | { shuffle?: boolean; repeat?: boolean; heart?: boolean; } | { shuffle: true, repeat: true, heart: true } | Controls visibility of additional player controls. |