Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

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.

campana-notificacion.tsx64 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/campana-notificacion

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
initial?numberOpcional.
demoEvery?numberSimula un aviso nuevo cada cierto tiempo (ms). 0 lo desactiva.
onOpen?() => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)