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

How to build an animated notification stack in React

An animated notification stack in React cycles through a data array on a setInterval, prepends each new item to a bounded visible list, and animates entries and exits with Framer Motion's AnimatePresence and layout prop, producing the staggered push-up effect where older notifications scroll off the top as new ones arrive at the bottom.

  • Stack: React 18 + Framer Motion 11, zero icon library, ~150 lines.
  • Core Framer Motion APIs: AnimatePresence (mode='popLayout'), motion.div layout, initial/animate/exit.
  • Interval defaults to 2 000 ms; maxVisible defaults to 4, both configurable as props.
  • Avatar is aria-hidden; the notification text is plain DOM, screen-reader friendly.
  • The live timestamp ticks every second via a separate setInterval driving a forceUpdate.

This banner component renders a self-cycling stream of notification cards to simulate live platform activity, signups, purchases, reviews, whatever fits the product. Cards appear at the bottom, older ones push upward, and the stack never grows past four items. The effect reads as real-time social proof without any backend dependency.

Anatomy

The section has two parts. At the top, a centered header block holds a pill badge with a pulsing green dot labeled 'Live', a large headline, and a subtitle paragraph. Below it, a 480 px-wide flex column renders each notification as a card: a 36 px colored avatar circle on the left, a right column with the sender name, a relative timestamp flushed to the right, and the message text truncated to a single line with text-overflow:ellipsis.

How it works

Two `useEffect` hooks handle the lifecycle. The first runs once on mount to seed the visible array with the first two notifications, giving the stack an immediate populated appearance. The second sets a recurring interval at `intervalMs` (default 2 000 ms) that calls `addNotif`, which prepends the next notification in the rotation and slices the array to `maxVisible`. A third interval fires every second and calls `forceUpdate` to refresh the relative timestamps without touching the notification data. Framer Motion's `AnimatePresence mode='popLayout'` handles the visual transitions: new items enter from below (`y: 20, opacity: 0, scale: 0.97`), existing items shift upward via the `layout` prop, and departing items exit upward (`y: -16, opacity: 0, scale: 0.96`), all driven by a custom `[0.16, 1, 0.3, 1]` spring-like cubic-bezier.

How to build it in React

  1. Define the data shape and seed the initial state

    Each notification needs an `id`, `initials`, `name`, `message`, `color`, and a `timestamp` offset in seconds (used to make relative times feel spread out). On mount, pre-populate with the first two items so the stack is never empty on first render.

    useEffect(() => {
      setVisible(
        notifications.slice(0, 2).map((n, i) => ({
          ...n,
          id: Date.now() - i * 100,
          spawnedAt: Date.now() - (i + 1) * 6000,
        }))
      );
      setTick(2);
    }, []);
  2. Cycle new notifications on an interval

    Use a `setInterval` to prepend the next item from your array each tick. Slice the result to `maxVisible` so the list never grows unbounded. Track the current index with a `tick` counter incremented on each call.

    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. Animate with AnimatePresence and layout

    Wrap the list in `<AnimatePresence initial={false} mode='popLayout'>`. Give each `motion.div` a stable `key` (use `id`, not array index), the `layout` prop, plus `initial`, `animate`, and `exit` variants. The `layout` prop handles the push-up reflow automatically, no manual position math needed.

    <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. Keep timestamps alive with a second interval

    The relative time display ('il y a 3s', 'il y a 2min') needs to update every second without triggering a full data refresh. Add a separate interval that calls a dummy state setter to force a re-render each second. The timestamp calculation then reads `Date.now()` fresh on every render.

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

When to use it

Reach for this pattern on marketing landing pages where you need to convey platform activity and social proof without real-time data, SaaS signups, e-commerce purchases, course enrollments. It works best above a pricing section or before a primary CTA. Avoid it on dashboards or product UI where looping fake data would confuse users expecting real information. On mobile it renders fine, but keep messages short since the single-line truncation removes context on narrow screens.

Used by

  • Fomo, A dedicated social proof SaaS product that displays real-time purchase and signup notifications as overlaid toast cards on customer websites.
  • Proof, Conversion optimization tool whose core UI is a looping notification feed showing recent visitor signups and actions to new visitors.
  • Gumroad, Creator marketplace that shows live purchase activity notifications on product pages to reinforce demand.
  • TrustPulse, WordPress and WooCommerce plugin that injects animated activity notification stacks (purchases, signups, reviews) to boost conversions.

FAQ

Why use `mode='popLayout'` instead of the default AnimatePresence mode?

The default mode waits for an exiting element to finish before inserting the entering one, which creates a visible gap. `popLayout` lets Framer Motion remove the exiting element from the document flow immediately while still animating it as an overlay, so the remaining cards reposition without a jump.

Can I replace the fake data with real WebSocket events?

Yes. Replace the `setInterval` with a WebSocket or Server-Sent Events listener that calls `setVisible` the same way, prepend and slice. The AnimatePresence animation layer doesn't care where the data comes from.

How do I prevent layout shift when the component first renders?

Set `initial={false}` on `AnimatePresence`, this skips the mount animation for items already present on first render, so the pre-seeded notifications appear instantly without sliding in.

The animation stutters on low-end devices. How to fix it?

Add `will-change: transform, opacity` to the card style to hint the browser to promote it to a composited layer. Reducing `maxVisible` from 4 to 2 also cuts the number of simultaneously animated DOM nodes in half.

"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

Reviews

React Live Activity Feed with Framer Motion, Code + Tutorial