Cloche de notification
Touchez la cloche : elle oscille sur un ressort, le battant suit en décalé, deux ondes apparaissent et le compteur défile.
Réglages
Code
import { useEffect, useState } from "react";
import {
AnimatePresence,
animate,
motion,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
} from "framer-motion";
const BELL_BODY =
"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326";
const BELL_CLAPPER = "M10.268 21a2 2 0 0 0 3.464 0";
interface BellShakeProps {
color?: string;
start?: number;
}
export default function BellShake({ color = "#ff453a", start = 3 }: BellShakeProps) {
const [count, setCount] = useState(start);
const [rings, setRings] = useState(0);
const reduce = useReducedMotion();
const rot = useMotionValue(0);
const clapX = useSpring(
useTransform(rot, (r) => r * -0.12),
{ stiffness: 260, damping: 7 },
);
const bump = useMotionValue(1);
useEffect(() => {
setCount(start);
}, [start]);
const ring = () => {
setCount((c) => c + 1);
setRings((r) => r + 1);
if (reduce) return;
animate(rot, 0, { type: "spring", velocity: -620, stiffness: 520, damping: 6 });
animate(bump, 1, { type: "spring", velocity: 5, stiffness: 600, damping: 12 });
};
return (
<motion.button
type="button"
onClick={ring}
aria-label={"Notifications : " + count + " non lues"}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.92 }}
transition={{ type: "spring", stiffness: 500, damping: 22 }}
className="relative grid h-14 w-14 place-items-center rounded-full bg-white text-[#1d1d1f] shadow-[0_0_0_1px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.06),0_10px_28px_-6px_rgba(0,0,0,0.14)] outline-none focus-visible:ring-2 focus-visible:ring-black/20 dark:bg-[#2c2c2e] dark:text-[#f5f5f7] dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_10px_28px_-6px_rgba(0,0,0,0.6)] dark:focus-visible:ring-white/30"
>
<span className="relative h-6 w-6">
<motion.svg
viewBox="0 0 24 24"
className="absolute inset-0 h-6 w-6"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
style={{ rotate: rot, originY: 0.08 }}
aria-hidden="true"
>
<path d={BELL_BODY} />
<motion.path d={BELL_CLAPPER} style={{ x: clapX }} />
</motion.svg>
{rings > 0 && !reduce && (
<svg
key={rings}
viewBox="0 0 24 24"
className="pointer-events-none absolute inset-0 h-6 w-6 overflow-visible"
fill="none"
stroke={color}
strokeWidth={1.5}
strokeLinecap="round"
aria-hidden="true"
>
<motion.path
d="M2.6 4.2C1.4 5.6 0.8 7.2 0.8 9"
initial={{ opacity: 0, x: 1 }}
animate={{ opacity: [0, 1, 0], x: -2.5 }}
transition={{ duration: 0.55, ease: "easeOut" }}
/>
<motion.path
d="M21.4 4.2C22.6 5.6 23.2 7.2 23.2 9"
initial={{ opacity: 0, x: -1 }}
animate={{ opacity: [0, 1, 0], x: 2.5 }}
transition={{ duration: 0.55, ease: "easeOut" }}
/>
</svg>
)}
</span>
<AnimatePresence>
{count > 0 && (
<motion.span
className="absolute -right-0.5 -top-0.5"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 20 }}
>
<motion.span
className="flex h-5 min-w-5 items-center justify-center overflow-hidden rounded-full px-1.5 text-[11px] font-semibold tabular-nums text-white ring-[2.5px] ring-[#fafafa] dark:ring-[#161617]"
style={{ backgroundColor: color, scale: bump }}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={count}
initial={{ y: 12, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -12, opacity: 0 }}
transition={{ type: "spring", stiffness: 600, damping: 30 }}
>
{count > 99 ? "99+" : count}
</motion.span>
</AnimatePresence>
</motion.span>
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
}
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
Une cloche de notification animée rend les nouveautés visibles sans bannière tapageuse. Placez-la dans l’en-tête d’un tableau de bord, d’un réseau social ou d’une plateforme de cours en ligne. La cloche oscille avec une vraie physique de ressort, le battant suit avec un temps de retard, deux petites ondes apparaissent et le badge rouge passe au nouveau chiffre.
Dans la démo, chaque toucher ajoute un, mais dans votre application elle ne devrait s’agiter qu’à l’arrivée d’une notification. Jamais en boucle. Le badge s’arrête à 99+, et vous choisissez le nombre de non-lus au départ.