Campana de notificaciones que se agita
La campana se agita cuando llega un aviso, el contador salta con un resorte y se limpia al abrir.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useState, type CSSProperties } from "react";
/** Campana de notificaciones: se agita al llegar un aviso nuevo, el contador salta con un resorte y se limpia al abrir. React + TypeScript, sin dependencias. */
export type CampanaNotificacionProps = {
initial?: number;
/** Simula un aviso nuevo cada cierto tiempo (ms). 0 lo desactiva. */
demoEvery?: number;
onOpen?: () => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcmcn-root { box-sizing: border-box; display: inline-block; color: #1f2430; font: 700 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcmcn-root *, .gcmcn-root *::before, .gcmcn-root *::after { box-sizing: border-box; }
.gcmcn-root :focus-visible { outline: 3px solid #2563eb; outline-offset: 4px; border-radius: 50%; }
@media (prefers-reduced-motion: reduce) { .gcmcn-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcmcn-btn { position: relative; display: grid; place-items: center; width: 56px; height: 56px; padding: 0; border: 1px solid #e3e6ee; border-radius: 50%; background: #fff; color: inherit; cursor: pointer; box-shadow: 0 1px 2px rgba(20, 30, 50, .1); transition: transform .15s; -webkit-tap-highlight-color: transparent; }
.gcmcn-btn:active { transform: scale(.94); }
.gcmcn-bell { transform-origin: 50% 8%; }
.gcmcn-btn[data-ring="true"] .gcmcn-bell { animation: gcmcn-ring .9s ease-in-out; }
.gcmcn-clap { transform-origin: 50% 0; }
.gcmcn-btn[data-ring="true"] .gcmcn-clap { animation: gcmcn-clap .9s ease-in-out; }
.gcmcn-badge { position: absolute; top: -4px; right: -4px; min-width: 22px; height: 22px; padding: 0 6px; display: grid; place-items: center; border-radius: 999px; background: #e5265a; color: #fff; box-shadow: 0 0 0 3px #fff; font-variant-numeric: tabular-nums; animation: gcmcn-pop .5s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes gcmcn-ring { 0%, 100% { transform: rotate(0); } 15% { transform: rotate(16deg); } 30% { transform: rotate(-14deg); } 45% { transform: rotate(10deg); } 60% { transform: rotate(-8deg); } 75% { transform: rotate(4deg); } }
@keyframes gcmcn-clap { 15% { transform: translateX(3px); } 30% { transform: translateX(-3px); } 45% { transform: translateX(2px); } 60% { transform: translateX(-2px); } }
@keyframes gcmcn-pop { 0% { transform: scale(0); } 100% { transform: scale(1); } }`;
export function CampanaNotificacion({ initial = 3, demoEvery = 6000, onOpen, className = "", style }: CampanaNotificacionProps) {
const [count, setCount] = useState(initial);
const [ring, setRing] = useState(0);
useEffect(() => {
if (!demoEvery) return;
const id = window.setInterval(() => {
if (document.hidden) return;
setCount((value) => value + 1);
setRing((value) => value + 1);
}, demoEvery);
return () => window.clearInterval(id);
}, [demoEvery]);
return (
<span className={`gcmcn-root ${className}`} style={style}>
<style>{CSS}</style>
<button key={ring} className="gcmcn-btn" type="button" data-ring={ring > 0 ? "true" : undefined} aria-label={count ? `Notificaciones, ${count} sin leer` : "Notificaciones"} onClick={() => { setCount(0); onOpen?.(); }}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<g className="gcmcn-bell">
<path d="M6 9a6 6 0 1 1 12 0c0 6 2 7.5 2 7.5H4S6 15 6 9z" />
</g>
<path className="gcmcn-clap" d="M10 20a2 2 0 0 0 4 0" />
</svg>
{count > 0 ? (
<span key={count} className="gcmcn-badge" aria-hidden="true">
{count > 99 ? "99+" : count}
</span>
) : null}
</button>
</span>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/campana-notificacionEjemplo de uso de Campana de notificaciones que se agita
Después de copiar campana-notificacion.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { CampanaNotificacion } from "./campana-notificacion";
export default function Ejemplo() {
return <CampanaNotificacion />;
}Propiedades de CampanaNotificacion
CampanaNotificacion acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
initial? | number | Opcional. |
demoEvery? | number | Simula un aviso nuevo cada cierto tiempo (ms). 0 lo desactiva. |
onOpen? | () => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 64 líneas (4 KB) y define 3 animaciones con @keyframes. Esto es lo que hace Campana de notificaciones que se agita por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".