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

Créer une pile de notifications animées en React

Une pile de notifications animées en React parcourt un tableau de données via setInterval, ajoute chaque nouvel élément en tête d'une liste bornée, et anime les entrées/sorties avec AnimatePresence et la prop layout de Framer Motion, ce qui produit l'effet de poussée vers le haut où les anciennes notifications quittent par le sommet pendant que les nouvelles arrivent par le bas.

  • Stack : React 18 + Framer Motion 11, aucune librairie d'icônes, ~150 lignes.
  • API Framer Motion clés : AnimatePresence (mode='popLayout'), motion.div layout, initial/animate/exit.
  • L'intervalle est 2 000 ms par défaut ; maxVisible vaut 4, les deux sont configurables en props.
  • L'avatar est aria-hidden ; le texte des notifications est du DOM ordinaire, compatible lecteurs d'écran.
  • Le timestamp relatif se met à jour chaque seconde via un setInterval distinct qui force un re-render.

Ce composant banner affiche un flux de cards de notifications en boucle pour simuler une activité de plateforme en direct, inscriptions, achats, avis, selon le produit. Les cards apparaissent en bas, les anciennes remontent, et la pile ne dépasse jamais quatre éléments. L'effet donne une impression de preuve sociale en temps réel, sans aucune dépendance backend.

Anatomie

La section se compose de deux parties. En haut, un bloc d'en-tête centré contient un badge pill avec un point vert pulsé libellé 'Live', un grand titre et un paragraphe de sous-titre. En dessous, une colonne flex de 480 px de large rend chaque notification sous forme de card : un cercle avatar coloré de 36 px à gauche, une colonne droite avec le nom de l'expéditeur, un timestamp relatif aligné à droite, et le texte du message tronqué sur une seule ligne via text-overflow:ellipsis.

Comment ça marche

Deux hooks `useEffect` gèrent le cycle de vie. Le premier s'exécute au montage pour pré-remplir le tableau visible avec les deux premières notifications, donnant immédiatement l'apparence d'une stack peuplée. Le second pose un intervalle récurrent à `intervalMs` (2 000 ms par défaut) qui appelle `addNotif`, laquelle ajoute la notification suivante en tête de liste et tronque à `maxVisible`. Un troisième intervalle se déclenche chaque seconde pour appeler `forceUpdate` et rafraîchir les timestamps relatifs sans toucher aux données. `AnimatePresence mode='popLayout'` de Framer Motion gère les transitions visuelles : les nouveaux éléments entrent par le bas (`y: 20, opacity: 0, scale: 0.97`), les existants remontent via la prop `layout`, et les sortants partent vers le haut (`y: -16, opacity: 0, scale: 0.96`), tous pilotés par une courbe de Bézier cubique `[0.16, 1, 0.3, 1]` proche d'un spring.

Comment le coder en React

  1. Définir la structure des données et initialiser l'état

    Chaque notification a besoin d'un `id`, d'`initials`, d'un `name`, d'un `message`, d'une `color` et d'un `timestamp` en secondes (utilisé pour que les temps relatifs paraissent espacés). Au montage, pré-remplis avec les deux premiers éléments pour que la stack ne soit jamais vide au premier rendu.

    useEffect(() => {
      setVisible(
        notifications.slice(0, 2).map((n, i) => ({
          ...n,
          id: Date.now() - i * 100,
          spawnedAt: Date.now() - (i + 1) * 6000,
        }))
      );
      setTick(2);
    }, []);
  2. Faire défiler les notifications sur un intervalle

    Utilise un `setInterval` pour ajouter l'élément suivant de ton tableau à chaque tick. Tronque le résultat à `maxVisible` pour que la liste ne grandisse pas indéfiniment. Suis l'index courant avec un compteur `tick` incrémenté à chaque appel.

    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. Animer avec AnimatePresence et layout

    Enveloppe la liste dans `<AnimatePresence initial={false} mode='popLayout'>`. Donne à chaque `motion.div` une `key` stable (utilise `id`, pas l'index), la prop `layout`, plus les variants `initial`, `animate` et `exit`. La prop `layout` gère automatiquement le repositionnement vers le haut, pas de calcul de position manuel.

    <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. Garder les timestamps vivants avec un second intervalle

    L'affichage du temps relatif ('il y a 3s', 'il y a 2min') doit se rafraîchir chaque seconde sans déclencher une mise à jour des données. Ajoute un intervalle séparé qui appelle un setter d'état factice pour forcer un re-render toutes les secondes. Le calcul du timestamp lit alors `Date.now()` à chaque rendu.

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

Quand l'utiliser

Ce pattern est à utiliser sur les landing pages marketing où tu dois transmettre l'activité de la plateforme et la preuve sociale sans données en temps réel, inscriptions SaaS, achats e-commerce, inscriptions à des formations. Il fonctionne mieux au-dessus d'une section de tarifs ou avant un CTA principal. À éviter dans les dashboards ou l'UI produit, où des données fictives en boucle confondrait des utilisateurs qui s'attendent à des informations réelles. Sur mobile le rendu est correct, mais garde des messages courts car la troncature sur une ligne fait perdre du contexte sur les écrans étroits.

Utilisé par

  • Fomo, Un produit SaaS dédié à la preuve sociale qui affiche des notifications d'achat et d'inscription en temps réel sous forme de cards toast superposées sur les sites clients.
  • Proof, Outil d'optimisation de conversion dont l'interface principale est un flux de notifications en boucle montrant les inscriptions et actions récentes aux nouveaux visiteurs.
  • Gumroad, Marketplace de créateurs qui affiche des notifications d'achat en direct sur les pages produit pour renforcer la perception de la demande.
  • TrustPulse, Plugin WordPress et WooCommerce qui injecte des piles de notifications d'activité animées (achats, inscriptions, avis) pour augmenter les conversions.

FAQ

Pourquoi utiliser `mode='popLayout'` plutôt que le mode par défaut d'AnimatePresence ?

`popLayout` permet à Framer Motion de retirer l'élément sortant du flux du document immédiatement tout en l'animant en overlay, de sorte que les cards restantes se repositionnent sans saut. Le mode par défaut attendrait la fin de l'animation de sortie avant d'insérer la nouvelle, créant un blanc visible.

Peut-on remplacer les fausses données par de vrais événements WebSocket ?

Oui. Remplace le `setInterval` par un écouteur WebSocket ou SSE qui appelle `setVisible` de la même façon, ajout en tête et troncature. La couche d'animation AnimatePresence ne dépend pas de la source des données.

Comment éviter le layout shift au premier rendu du composant ?

Passe `initial={false}` à `AnimatePresence`, cela désactive l'animation au montage pour les éléments déjà présents au premier rendu, donc les notifications pré-chargées apparaissent instantanément sans glisser.

L'animation accroche sur les appareils peu puissants. Comment y remédier ?

Ajoute `will-change: transform, opacity` au style de la card pour indiquer au navigateur de la promouvoir sur une couche composite. Réduire `maxVisible` de 4 à 2 divise aussi par deux le nombre de nœuds DOM animés simultanément.

"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

Avis

Flux d'activité live React avec Framer Motion, Code +