Music Card
Original · freeA compact music player card with artwork and transport controls.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-music.json"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { useReducedMotion } from "motion/react";
type Track = {
id: string;
title: string;
artist: string;
album: string;
art: string;
duration: number; // seconds
};
const PLAYLIST: Track[] = [
{
id: "t1",
title: "Blinding Lights",
artist: "The Weeknd",
album: "After Hours",
art: "/img/gallery/g07.webp",
duration: 200,
},
{
id: "t2",
title: "As It Was",
artist: "Harry Styles",
album: "Harry's House",
art: "/img/gallery/g12.webp",
duration: 167,
},
{
id: "t3",
title: "Flowers",
artist: "Miley Cyrus",
album: "Endless Summer Vacation",
art: "/img/gallery/g21.webp",
duration: 200,
},
{
id: "t4",
title: "Midnight City",
artist: "M83",
album: "Hurry Up, We're Dreaming",
art: "/img/gallery/g29.webp",
duration: 244,
},
];
function fmt(sec: number): string {
const s = Math.max(0, Math.floor(sec));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r.toString().padStart(2, "0")}`;
}
/* ---------- inline glyphs ---------- */
function PlayIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<path d="M8 5.14v13.72a1 1 0 0 0 1.5.86l11.14-6.86a1 1 0 0 0 0-1.72L9.5 4.28A1 1 0 0 0 8 5.14Z" />
</svg>
);
}
function PauseIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<rect x="6" y="5" width="4" height="14" rx="1.2" />
<rect x="14" y="5" width="4" height="14" rx="1.2" />
</svg>
);
}
function PrevIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<path d="M7 5a1 1 0 0 1 2 0v5.2l8.5-5.06A1 1 0 0 1 19 6v12a1 1 0 0 1-1.5.86L9 13.8V19a1 1 0 0 1-2 0V5Z" />
</svg>
);
}
function NextIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
<path d="M17 5a1 1 0 0 0-2 0v5.2L6.5 5.14A1 1 0 0 0 5 6v12a1 1 0 0 0 1.5.86L15 13.8V19a1 1 0 0 0 2 0V5Z" />
</svg>
);
}
function ShuffleIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M16 3h5v5" />
<path d="M4 20 21 3" />
<path d="M21 16v5h-5" />
<path d="M15 15l6 6" />
<path d="M4 4l5 5" />
</svg>
);
}
function RepeatIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="m17 2 4 4-4 4" />
<path d="M3 11v-1a4 4 0 0 1 4-4h14" />
<path d="m7 22-4-4 4-4" />
<path d="M21 13v1a4 4 0 0 1-4 4H3" />
</svg>
);
}
function HeartIcon({ filled, className }: { filled: boolean; className?: string }) {
return (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8Z" />
</svg>
);
}
function VolumeIcon({ muted, className }: { muted: boolean; className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
<path d="M11 5 6 9H2v6h4l5 4V5Z" fill="currentColor" stroke="none" />
{muted ? (
<>
<path d="m22 9-6 6" />
<path d="m16 9 6 6" />
</>
) : (
<>
<path d="M15.5 8.5a5 5 0 0 1 0 7" />
<path d="M18.5 5.5a9 9 0 0 1 0 13" />
</>
)}
</svg>
);
}
/* ---------- the player ---------- */
function MusicPlayerCard({ compact = false }: { compact?: boolean }) {
const reduce = useReducedMotion();
const [index, setIndex] = useState(0);
const [playing, setPlaying] = useState(false);
const [elapsed, setElapsed] = useState(42);
const [liked, setLiked] = useState<Record<string, boolean>>({ t1: true });
const [shuffle, setShuffle] = useState(false);
const [repeat, setRepeat] = useState(false);
const [muted, setMuted] = useState(false);
const [scrubbing, setScrubbing] = useState(false);
const track = PLAYLIST[index];
const rafRef = useRef<number | null>(null);
const lastRef = useRef<number>(0);
const goNext = useCallback(() => {
setElapsed(0);
setIndex((i) => {
if (shuffle) {
if (PLAYLIST.length === 1) return i;
let n = i;
while (n === i) n = Math.floor(Math.random() * PLAYLIST.length);
return n;
}
return (i + 1) % PLAYLIST.length;
});
}, [shuffle]);
const goPrev = useCallback(() => {
if (elapsed > 3) {
setElapsed(0);
return;
}
setElapsed(0);
setIndex((i) => (i - 1 + PLAYLIST.length) % PLAYLIST.length);
}, [elapsed]);
// simulated playback clock
useEffect(() => {
if (!playing || scrubbing) return;
lastRef.current = performance.now();
const tick = (now: number) => {
const dt = (now - lastRef.current) / 1000;
lastRef.current = now;
setElapsed((e) => {
const next = e + dt;
if (next >= track.duration) {
if (repeat) return 0;
queueMicrotask(goNext);
return track.duration;
}
return next;
});
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [playing, scrubbing, track.duration, repeat, goNext]);
const pct = Math.min(100, (elapsed / track.duration) * 100);
const isLiked = !!liked[track.id];
const seekRef = useRef<HTMLInputElement | null>(null);
return (
<div
className={[
"group/card w-full overflow-hidden rounded-3xl border text-left shadow-xl shadow-slate-900/5 backdrop-blur",
"border-slate-200/80 bg-white/90",
"dark:border-white/10 dark:bg-slate-900/70 dark:shadow-black/40",
compact ? "max-w-sm" : "max-w-md",
].join(" ")}
>
{/* artwork */}
<div className={compact ? "p-4" : "p-5"}>
<div className="relative overflow-hidden rounded-2xl">
<div
className={[
"relative aspect-square w-full",
playing && !reduce ? "cardmus-float" : "",
].join(" ")}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={track.art}
alt={`${track.album} album cover`}
loading="lazy"
draggable={false}
className="h-full w-full select-none object-cover"
/>
{/* subtle sheen */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-tr from-black/25 via-transparent to-white/10" />
{/* now-playing pill */}
<div className="absolute left-3 top-3 flex items-center gap-1.5 rounded-full bg-black/45 px-2.5 py-1 text-[11px] font-medium text-white backdrop-blur-sm">
<span className="flex h-3 items-end gap-[2px]" aria-hidden="true">
{[0, 1, 2].map((b) => (
<span
key={b}
className={[
"w-[3px] rounded-full bg-emerald-400",
playing && !reduce ? `cardmus-bar cardmus-bar-${b}` : "",
].join(" ")}
style={playing && !reduce ? undefined : { height: "40%" }}
/>
))}
</span>
{playing ? "Now Playing" : "Paused"}
</div>
{/* like */}
<button
type="button"
onClick={() => setLiked((m) => ({ ...m, [track.id]: !m[track.id] }))}
aria-pressed={isLiked}
aria-label={isLiked ? `Remove ${track.title} from favourites` : `Add ${track.title} to favourites`}
className={[
"absolute right-3 top-3 grid h-9 w-9 place-items-center rounded-full backdrop-blur-sm transition",
"outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black/30",
isLiked
? "bg-rose-500/90 text-white"
: "bg-black/40 text-white hover:bg-black/55",
].join(" ")}
>
<HeartIcon filled={isLiked} className={["h-[18px] w-[18px]", isLiked && !reduce ? "cardmus-pop" : ""].join(" ")} />
</button>
</div>
</div>
</div>
{/* meta + controls */}
<div className={compact ? "px-4 pb-5" : "px-6 pb-6"}>
<p className="text-[11px] font-semibold uppercase tracking-widest text-indigo-500 dark:text-indigo-400">
{track.album}
</p>
<h3 className="mt-1 truncate text-lg font-bold text-slate-900 dark:text-white">
{track.title}
</h3>
<p className="truncate text-sm text-slate-500 dark:text-slate-400">{track.artist}</p>
{/* seek */}
<div className="mt-4">
<div className="relative h-1.5 w-full rounded-full bg-slate-200 dark:bg-white/10">
<div
className="absolute inset-y-0 left-0 rounded-full bg-slate-900 dark:bg-white"
style={{ width: `${pct}%` }}
/>
<span
className="absolute top-1/2 h-3.5 w-3.5 -translate-y-1/2 -translate-x-1/2 rounded-full bg-slate-900 shadow ring-2 ring-white transition-transform group-focus-within/card:scale-100 dark:bg-white dark:ring-slate-900"
style={{ left: `${pct}%` }}
aria-hidden="true"
/>
<input
ref={seekRef}
type="range"
min={0}
max={Math.floor(track.duration)}
step={1}
value={Math.floor(elapsed)}
onChange={(e) => setElapsed(Number(e.target.value))}
onPointerDown={() => setScrubbing(true)}
onPointerUp={() => setScrubbing(false)}
onBlur={() => setScrubbing(false)}
aria-label={`Seek. ${fmt(elapsed)} of ${fmt(track.duration)}`}
className="absolute inset-0 h-full w-full cursor-pointer appearance-none bg-transparent opacity-0"
/>
</div>
<div className="mt-1.5 flex justify-between text-[11px] font-medium tabular-nums text-slate-500 dark:text-slate-400">
<span>{fmt(elapsed)}</span>
<span>-{fmt(track.duration - elapsed)}</span>
</div>
</div>
{/* transport */}
<div className="mt-4 flex items-center justify-between">
<button
type="button"
onClick={() => setShuffle((v) => !v)}
aria-pressed={shuffle}
aria-label="Shuffle"
className={[
"grid h-9 w-9 place-items-center rounded-full transition outline-none",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
shuffle
? "text-indigo-600 dark:text-indigo-400"
: "text-slate-400 hover:text-slate-700 dark:text-slate-500 dark:hover:text-slate-200",
].join(" ")}
>
<ShuffleIcon className="h-[18px] w-[18px]" />
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={goPrev}
aria-label="Previous track"
className="grid h-10 w-10 place-items-center rounded-full text-slate-700 transition hover:bg-slate-100 active:scale-95 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-200 dark:hover:bg-white/10 dark:focus-visible:ring-offset-slate-900"
>
<PrevIcon className="h-5 w-5" />
</button>
<button
type="button"
onClick={() => setPlaying((p) => !p)}
aria-pressed={playing}
aria-label={playing ? `Pause ${track.title}` : `Play ${track.title}`}
className="grid h-14 w-14 place-items-center rounded-full bg-slate-900 text-white shadow-lg shadow-slate-900/25 transition hover:scale-[1.04] active:scale-95 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:shadow-black/40 dark:focus-visible:ring-offset-slate-900"
>
{playing ? <PauseIcon className="h-6 w-6" /> : <PlayIcon className="h-6 w-6 translate-x-[1px]" />}
</button>
<button
type="button"
onClick={goNext}
aria-label="Next track"
className="grid h-10 w-10 place-items-center rounded-full text-slate-700 transition hover:bg-slate-100 active:scale-95 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-200 dark:hover:bg-white/10 dark:focus-visible:ring-offset-slate-900"
>
<NextIcon className="h-5 w-5" />
</button>
</div>
<button
type="button"
onClick={() => setRepeat((v) => !v)}
aria-pressed={repeat}
aria-label="Repeat"
className={[
"grid h-9 w-9 place-items-center rounded-full transition outline-none",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
repeat
? "text-indigo-600 dark:text-indigo-400"
: "text-slate-400 hover:text-slate-700 dark:text-slate-500 dark:hover:text-slate-200",
].join(" ")}
>
<RepeatIcon className="h-[18px] w-[18px]" />
</button>
</div>
{/* volume row */}
<div className="mt-4 flex items-center gap-3 border-t border-slate-100 pt-4 dark:border-white/5">
<button
type="button"
onClick={() => setMuted((v) => !v)}
aria-pressed={muted}
aria-label={muted ? "Unmute" : "Mute"}
className="grid h-8 w-8 shrink-0 place-items-center rounded-full text-slate-500 transition hover:text-slate-800 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
>
<VolumeIcon muted={muted} className="h-[18px] w-[18px]" />
</button>
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-200 dark:bg-white/10">
<div
className="h-full rounded-full bg-slate-400 transition-[width] dark:bg-slate-500"
style={{ width: muted ? "0%" : "68%" }}
/>
</div>
<span className="w-8 text-right text-[11px] font-medium tabular-nums text-slate-400 dark:text-slate-500">
{muted ? "0" : "68"}
</span>
</div>
</div>
</div>
);
}
/* ---------- showcase ---------- */
export default function CardMusic() {
return (
<section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:px-10">
<style>{`
@keyframes cardmus-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes cardmus-eq {
0%, 100% { height: 30%; }
50% { height: 100%; }
}
@keyframes cardmus-pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
100% { transform: scale(1); }
}
.cardmus-float { animation: cardmus-float 5s ease-in-out infinite; }
.cardmus-bar { animation: cardmus-eq 0.9s ease-in-out infinite; }
.cardmus-bar-0 { animation-delay: 0s; }
.cardmus-bar-1 { animation-delay: 0.25s; }
.cardmus-bar-2 { animation-delay: 0.5s; }
.cardmus-pop { animation: cardmus-pop 0.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
.cardmus-float, .cardmus-bar, .cardmus-pop { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mb-12 text-center">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
Music player card
</h2>
<p className="mx-auto mt-3 max-w-xl text-sm text-slate-500 dark:text-slate-400">
Tactile transport controls, a seekable timeline, working like & shuffle
state, and a live equalizer while playing.
</p>
</div>
<div className="flex flex-wrap items-start justify-center gap-8">
<MusicPlayerCard />
<MusicPlayerCard compact />
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

