Campana de notificaciones
Toca la campana: oscila con un muelle, el badajo va a destiempo, salen dos ondas y el contador rueda hacia arriba.
Ajustes
Código
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={"Notificaciones: " + count + " sin leer"}
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 para IA
Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.
Más en Microinteracciones
Dónde usarlo
Una campana de notificaciones animada hace que lo nuevo no pase desapercibido sin necesidad de un banner chillón. Colócala en la cabecera de un panel, una red social o una plataforma de cursos. Oscila con física de muelle real, el badajo llega un instante tarde, salen dos ondas a los lados y la insignia roja rueda al nuevo número.
En la demo cada toque suma uno, pero en tu app debería agitarse cuando llega una notificación, y solo entonces. Nunca en bucle. La insignia se queda en 99+ como máximo, y puedes elegir con cuántos avisos sin leer empieza.