Luces de Madrid
Noche Temprana
Vinyle lecture/pause
Touchez le disque : lecture devient pause, le bras se pose sur les sillons et le vinyle accélère puis freine avec inertie.
Réglages
Code
import { useEffect, useState } from "react";
import { motion, useAnimationFrame, useMotionValue, useReducedMotion, useSpring } from "framer-motion";
const PLAY_L = "M8 5.5 L13.5 8.75 L13.5 15.25 L8 18.5 Z";
const PLAY_R = "M13.5 8.75 L19 12 L19 12 L13.5 15.25 Z";
const PAUSE_L = "M7 5.5 L10.5 5.5 L10.5 18.5 L7 18.5 Z";
const PAUSE_R = "M13.5 5.5 L17 5.5 L17 18.5 L13.5 18.5 Z";
const vinylMorph = { type: "spring" as const, stiffness: 420, damping: 30 };
interface VinylPlayerProps {
title?: string;
artist?: string;
color?: string;
}
export default function VinylPlayer({
title = "Luces de Madrid",
artist = "Noche Temprana",
color = "#ff5a36",
}: VinylPlayerProps) {
const [playing, setPlaying] = useState(false);
const reduce = useReducedMotion();
const speed = useSpring(0, { stiffness: 26, damping: 12 });
const spin = useMotionValue(0);
useEffect(() => {
speed.set(playing ? 1 : 0);
}, [playing, speed]);
useAnimationFrame((_, delta) => {
const s = speed.get();
if (reduce || s < 0.001) return;
spin.set((spin.get() + delta * 0.2 * s) % 360);
});
return (
<div className="flex flex-col items-center gap-4">
<div className="relative h-[200px] w-[232px]">
<motion.button
type="button"
aria-label={playing ? "Pause " + title : "Lire " + title}
aria-pressed={playing}
onClick={() => setPlaying((p) => !p)}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 500, damping: 28 }}
className="absolute left-0 top-2 h-[184px] w-[184px] rounded-full outline-none focus-visible:ring-2 focus-visible:ring-black/25 focus-visible:ring-offset-4 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black"
style={{ boxShadow: "0 1px 2px rgba(0,0,0,0.25), 0 14px 34px -10px rgba(0,0,0,0.5)" }}
>
<motion.span
className="absolute inset-0 rounded-full"
style={{
rotate: spin,
background: "repeating-radial-gradient(circle at 50% 50%, #121212 0 1px, #1d1d1d 1px 2.5px)",
}}
>
<span className="absolute inset-[33%] rounded-full" style={{ backgroundColor: color }}>
<span className="absolute left-1/2 top-[12%] h-[3px] w-5 -translate-x-1/2 rounded-full bg-white/45" />
<span className="absolute bottom-[14%] left-1/2 h-[2px] w-3 -translate-x-1/2 rounded-full bg-black/20" />
</span>
</motion.span>
<span
className="pointer-events-none absolute inset-0 rounded-full"
style={{
background:
"conic-gradient(from 30deg, transparent 0deg, rgba(255,255,255,0.13) 30deg, transparent 70deg, transparent 180deg, rgba(255,255,255,0.08) 215deg, transparent 250deg)",
}}
aria-hidden="true"
/>
<span className="pointer-events-none absolute inset-0 rounded-full ring-1 ring-inset ring-white/10" aria-hidden="true" />
<span className="absolute inset-0 grid place-items-center text-white" aria-hidden="true">
<svg viewBox="0 0 24 24" className="h-6 w-6 drop-shadow-[0_1px_1px_rgba(0,0,0,0.25)]" fill="currentColor">
<motion.path initial={false} animate={{ d: playing ? PAUSE_L : PLAY_L }} transition={vinylMorph} />
<motion.path initial={false} animate={{ d: playing ? PAUSE_R : PLAY_R }} transition={vinylMorph} />
</svg>
</span>
</motion.button>
<motion.div
className="pointer-events-none absolute left-[206px] top-[18px] h-0 w-0"
initial={false}
animate={{ rotate: playing ? 22 : 0 }}
transition={
playing ? { type: "spring", stiffness: 90, damping: 13, delay: 0.12 } : { type: "spring", stiffness: 220, damping: 24 }
}
aria-hidden="true"
>
<svg className="absolute overflow-visible" style={{ left: -20, top: -24 }} width="40" height="172" viewBox="-20 -24 40 172">
<rect x="-5" y="-22" width="10" height="13" rx="3" className="fill-[#3a3a3c] dark:fill-[#8e8e93]" />
<path
d="M0 0 V106 L-9 124"
fill="none"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-[#c7c7cc] dark:stroke-[#636366]"
/>
<rect x="-15" y="120" width="11" height="17" rx="2.5" transform="rotate(27 -9.5 128)" className="fill-[#3a3a3c] dark:fill-[#aeaeb2]" />
<circle r="11" className="fill-[#e5e5ea] stroke-[#c7c7cc] dark:fill-[#3a3a3c] dark:stroke-[#48484a]" strokeWidth="1" />
<circle r="4" className="fill-[#aeaeb2] dark:fill-[#8e8e93]" />
</svg>
</motion.div>
</div>
<div className="text-center">
<p className="text-[15px] font-semibold tracking-tight text-[#1d1d1f] dark:text-[#f5f5f7]">{title}</p>
<p className="text-[13px] text-[#86868b]">{artist}</p>
</div>
</div>
);
}
Prompt pour l’IA
Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.
Plus dans Micro-interactions
Où l’utiliser
Cette animation de bouton lecture pause transforme un simple interrupteur en objet : un disque qui tourne avec son propre bras. Elle va bien au site d’un groupe ou d’un DJ, à un disquaire ou à un podcast à l’esprit rétro. Le disque met un instant à prendre sa vitesse et ralentit doucement, comme une vraie platine.
Ce n’est que la partie visuelle : il ne joue aucun son tout seul. Reliez-le à votre lecteur audio pour que le disque démarre et s’arrête avec le morceau. Adaptez la couleur de l’étiquette à votre pochette et modifiez titre et artiste.