Retour au catalogue

Notification Stack

Flux de notifications animées simulant une activité en temps réel. Cards toast avec avatar, message et timestamp relatif. Loop automatique toutes les 2s avec AnimatePresence, idéal pour la preuve sociale live.

bannersmedium Both Responsive a11y
boldcorporateminimalsaasecommerceagencystackedcentered
Theme

Cómo crear una pila de notificaciones animadas en React

Una pila de notificaciones animadas en React recorre un array de datos mediante un setInterval, añade cada nuevo elemento al principio de una lista acotada, y anima las entradas y salidas con AnimatePresence y la prop layout de Framer Motion, lo que produce el efecto de empuje hacia arriba donde las notificaciones antiguas salen por la parte superior mientras las nuevas llegan por abajo.

  • Stack: React 18 + Framer Motion 11, sin librería de iconos, ~150 líneas.
  • APIs clave de Framer Motion: AnimatePresence (mode='popLayout'), motion.div layout, initial/animate/exit.
  • El intervalo es 2 000 ms por defecto; maxVisible vale 4, ambos son configurables como props.
  • El avatar es aria-hidden; el texto de las notificaciones es DOM normal, compatible con lectores de pantalla.
  • El timestamp relativo se actualiza cada segundo mediante un setInterval independiente que fuerza un re-render.

Este componente banner muestra un flujo de cards de notificaciones en bucle para simular actividad de plataforma en directo, registros, compras, reseñas, según el producto. Las cards aparecen abajo, las antiguas suben, y la pila nunca supera los cuatro elementos. El efecto transmite una sensación de prueba social en tiempo real, sin ninguna dependencia de backend.

Anatomía

La sección se compone de dos partes. Arriba, un bloque de cabecera centrado contiene un badge pill con un punto verde pulsante etiquetado 'Live', un titular grande y un párrafo de subtítulo. Debajo, una columna flex de 480 px de ancho renderiza cada notificación como una card: un círculo avatar de color de 36 px a la izquierda, una columna derecha con el nombre del remitente, un timestamp relativo alineado a la derecha, y el texto del mensaje truncado a una sola línea mediante text-overflow:ellipsis.

Cómo funciona

Dos hooks `useEffect` gestionan el ciclo de vida. El primero se ejecuta al montar para rellenar el array visible con las dos primeras notificaciones, dando de inmediato la apariencia de una stack poblada. El segundo establece un intervalo recurrente a `intervalMs` (2 000 ms por defecto) que llama a `addNotif`, la cual añade la siguiente notificación al principio de la lista y la trunca a `maxVisible`. Un tercer intervalo se dispara cada segundo para llamar a `forceUpdate` y refrescar los timestamps relativos sin tocar los datos. `AnimatePresence mode='popLayout'` de Framer Motion gestiona las transiciones visuales: los nuevos elementos entran desde abajo (`y: 20, opacity: 0, scale: 0.97`), los existentes suben mediante la prop `layout`, y los salientes se van hacia arriba (`y: -16, opacity: 0, scale: 0.96`), todos impulsados por una curva de Bézier cúbica `[0.16, 1, 0.3, 1]` cercana a un spring.

Cómo crearlo en React

  1. Definir la estructura de los datos e inicializar el estado

    Cada notificación necesita un `id`, unos `initials`, un `name`, un `message`, un `color` y un `timestamp` en segundos (usado para que los tiempos relativos parezcan espaciados). Al montar, pre-rellena con los dos primeros elementos para que la stack nunca esté vacía en el primer renderizado.

    useEffect(() => {
      setVisible(
        notifications.slice(0, 2).map((n, i) => ({
          ...n,
          id: Date.now() - i * 100,
          spawnedAt: Date.now() - (i + 1) * 6000,
        }))
      );
      setTick(2);
    }, []);
  2. Hacer rotar las notificaciones en un intervalo

    Usa un `setInterval` para añadir el siguiente elemento de tu array en cada tick. Trunca el resultado a `maxVisible` para que la lista no crezca de forma ilimitada. Sigue el índice actual con un contador `tick` incrementado en cada llamada.

    const addNotif = useCallback(() => {
      setVisible((prev) => {
        const item = {
          ...notifications[tick % notifications.length],
          id: Date.now(),
          spawnedAt: Date.now(),
        };
        return [item, ...prev].slice(0, maxVisible);
      });
      setTick((t) => t + 1);
    }, [notifications, tick, maxVisible]);
    
    useEffect(() => {
      const t = setInterval(addNotif, intervalMs);
      return () => clearInterval(t);
    }, [addNotif, intervalMs]);
  3. Animar con AnimatePresence y layout

    Envuelve la lista en `<AnimatePresence initial={false} mode='popLayout'>`. Dale a cada `motion.div` una `key` estable (usa `id`, no el índice del array), la prop `layout`, además de los variants `initial`, `animate` y `exit`. La prop `layout` gestiona automáticamente el reposicionamiento hacia arriba, sin cálculo de posición manual.

    <AnimatePresence initial={false} mode="popLayout">
      {visible.map((notif) => (
        <motion.div
          key={notif.id}
          layout
          initial={{ opacity: 0, y: 20, scale: 0.97 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: -16, scale: 0.96 }}
          transition={{ layout: { duration: 0.3 }, opacity: { duration: 0.25 } }}
        >
          {/* card content */}
        </motion.div>
      ))}
    </AnimatePresence>
  4. Mantener los timestamps vivos con un segundo intervalo

    La visualización del tiempo relativo ('hace 3s', 'hace 2min') debe actualizarse cada segundo sin disparar una actualización completa de los datos. Añade un intervalo separado que llame a un setter de estado ficticio para forzar un re-render cada segundo. El cálculo del timestamp lee entonces `Date.now()` actualizado en cada render.

    const [, forceUpdate] = useState(0);
    useEffect(() => {
      const t = setInterval(() => forceUpdate((n) => n + 1), 1000);
      return () => clearInterval(t);
    }, []);

Cuándo usarlo

Recurre a este patrón en landing pages de marketing donde necesitas transmitir actividad de plataforma y prueba social sin datos en tiempo real, registros SaaS, compras de e-commerce, inscripciones a cursos. Funciona mejor encima de una sección de precios o antes de un CTA principal. Evítalo en dashboards o en la UI de producto, donde datos ficticios en bucle confundirían a usuarios que esperan información real. En móvil se renderiza bien, pero mantén los mensajes cortos porque el truncado a una sola línea hace perder contexto en pantallas estrechas.

Usado por

  • Fomo, Un producto SaaS dedicado a la prueba social que muestra notificaciones de compra y registro en tiempo real como cards toast superpuestas en los sitios de los clientes.
  • Proof, Herramienta de optimización de conversión cuya interfaz principal es un feed de notificaciones en bucle que muestra los registros y acciones recientes a los nuevos visitantes.
  • Gumroad, Marketplace de creadores que muestra notificaciones de compra en directo en las páginas de producto para reforzar la percepción de demanda.
  • TrustPulse, Plugin de WordPress y WooCommerce que inyecta pilas de notificaciones de actividad animadas (compras, registros, reseñas) para aumentar las conversiones.

FAQ

¿Por qué usar `mode='popLayout'` en lugar del modo por defecto de AnimatePresence?

El modo por defecto espera a que el elemento saliente termine antes de insertar el entrante, lo que crea un hueco visible. `popLayout` permite a Framer Motion retirar el elemento saliente del flujo del documento de inmediato mientras lo sigue animando como overlay, de modo que las cards restantes se reposicionan sin salto.

¿Puedo reemplazar los datos falsos por eventos WebSocket reales?

Sí. Reemplaza el `setInterval` por un listener de WebSocket o Server-Sent Events que llame a `setVisible` de la misma forma, añadiendo al principio y truncando. La capa de animación de AnimatePresence no le importa de dónde vienen los datos.

¿Cómo evito el layout shift en el primer renderizado del componente?

Pasa `initial={false}` a `AnimatePresence`, esto omite la animación de montaje para los elementos ya presentes en el primer renderizado, de modo que las notificaciones pre-cargadas aparecen al instante sin deslizarse.

La animación se entrecorta en dispositivos de gama baja. ¿Cómo se soluciona?

Añade `will-change: transform, opacity` al estilo de la card para sugerir al navegador que la promueva a una capa de composición. Reducir `maxVisible` de 4 a 2 también recorta a la mitad el número de nodos DOM animados simultáneamente.

"use client";

import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";

interface Notification {
  id: number;
  initials: string;
  name: string;
  message: string;
  color: string;
  timestamp: number;
}

interface NotificationItem extends Notification {
  spawnedAt: number;
}

interface BannersNotificationStackProps {
  notifications?: Notification[];
  intervalMs?: number;
  maxVisible?: number;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reseñas

Feed de actividad en vivo en React con Framer Motion, código