Creating a Vinyl Record Animation in React Using Framer Motion
Bring the charm of vinyl to your React projects with a sleek spinning record animation!
Table of Contents

Last weekend, while scrolling through Twitter, I came across a video of a spinning movie CD. (Original post: https://x.com/matthischmid/status/1858196401311654116) Coincidentally, I was listening to Linkin Park’s latest album at the time. Linkin Park holds a special place in my heart—they were the first band I ever listened to in a foreign language, and I’ve been a fan since I was seven.
I still feel a pang of sadness whenever I think of Chester 💔, but I’m also grateful that Emily continues to carry the torch. Their music has always been a source of inspiration for me.
Anyway, back to the topic! Inspired by that CD animation, I decided to create a spinning vinyl record component in React using Framer Motion. Let’s build it together!
Getting Started
This will be a simple component, so we’ll only need to install one package.
npm i framer-motion
- Create a new file in the
components
folder and name itvinyl-record.tsx
. - Copy the code below into your new file.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
const DiscVariants = {
hidden: {
top: "-10%",
x: "-50%",
opacity: 0.9,
},
visible: {
top: "-50%",
x: "-50%",
opacity: 1,
},
};
const CoverVariants = {
initial: { scale: 1 },
hover: {
scale: 1.05,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
},
},
};
export default function VinylRecord() {
const [isHovered, setIsHovered] = useState(false);
return (
<div className="flex items-center justify-center min-h-[80vh] bg-gray-100 p-4">
<div className="relative w-64 h-64">
<AnimatePresence>
{/* Vinyl Disc */}
<motion.div
className="absolute left-1/2 rounded-full z-0 w-60 h-60"
variants={DiscVariants}
initial="hidden"
animate={isHovered ? "visible" : "hidden"}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
}}
style={{
background:
"radial-gradient(circle at center, #0a0a0a 0%, #000000 80%)",
boxShadow: "inset 0 0 10px rgba(255, 255, 255, 0.5)",
}}
>
{/* Grooves Pattern */}
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1/2 h-1/2 rounded-full"
style={{
background:
"repeating-radial-gradient(circle at center, #333 0, #333 1px, transparent 1px, transparent 4px)",
}}
/>
{/* Center Label */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1/4 h-1/4 bg-gray-400 rounded-full flex items-center justify-center">
<motion.div
className="w-1/3 h-1/3 bg-white rounded-full"
animate={{ rotate: isHovered ? 360 : 0 }}
transition={{
duration: 3,
ease: "linear",
repeat: Infinity,
}}
/>
</div>
</motion.div>
</AnimatePresence>
{/* Record Cover */}
<motion.div
className="relative w-full h-full rounded shadow-xl z-10 overflow-hidden"
variants={CoverVariants}
initial="initial"
whileHover="hover"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Image
src="https://linkinpedia.com/w/images/thumb/f/fe/Album-From_Zero.png/800px-Album-From_Zero.png"
alt="Album Cover"
fill
sizes="(max-width: 256px) 100vw, 256px"
className="object-cover"
priority
/>
</motion.div>
</div>
</div>
);
}
That’s it! You can customize the size, animations, and image to match your preferences.