Social Proof Popup — Rotating Purchase Notification

Social Proof Popup · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Rotating event queue
Multiple distinct events cycle automatically, reading as ongoing activity rather than one static claim.
Zero-dependency generated avatars
Avatars are inline base64 SVGs generated per event color, with no image hosting or network request required.
Plausible relative timestamps
A fixed "X minutes ago" per event with a small live-style dot indicator, without the complexity of an actually ticking counter.
Permanent, respected dismissal
Closing the toast stops the rotation entirely for the session — it never reappears with the next queued event.
Auto-hide per toast
Each shown event automatically hides itself after about 5 seconds even without a dismiss click, keeping the page uncluttered.
Hover-revealed dismiss button
The ✕ control stays out of the way until hovered, avoiding visual clutter on a toast that's mostly meant to be glanced at.
Smooth slide-and-fade entrance
The toast animates in with opacity and transform only, staying smooth across every framework export.
Easily themeable accent colors
Each event's avatar color is just a hex value in its data object — no CSS changes needed to add new color variety.

About this UI Snippet

Social Proof Popup — Auto-Cycling Purchase Toasts with a Persistent Dismiss

Screenshot of the Social Proof Popup snippet rendered live

The small "Sarah from Austin just bought the Pro plan" bubble that drifts in near the corner of a landing page is a well-known conversion pattern — seeing recent, specific activity from real-seeming people builds more trust than a generic testimonial block. This snippet builds the complete rotating toast: a queue of events, automatic cycling on a timer, and a dismiss action that respects the user's choice for the rest of the session.

A queue of events, not a single static message

EVENTS is an array of { name, action, avatar, mins } objects, and cycle() advances through them in order (wrapping back to the start via rotateIndex % EVENTS.length), showing one at a time on a repeating interval. A single static "someone just bought this" message loses credibility the longer it sits on screen unchanged — rotating through several distinct, named events is what makes the pattern read as ongoing real activity rather than a fixed banner.

Generated avatar colors, not external images

Each event's avatar is a solid-color square generated on the fly as a tiny inline SVG, base64-encoded into a data: URI (avatarUri()) — no image hosting or placeholder service required, and no failed network request if the page is previewed offline. Swap this for a real photo URL per event once you have one; the <img> tag and its sizing don't need to change.

A relative timestamp that looks credible

Each event carries a mins value rendered as "X minutes ago" with a small pulsing-dot-style green indicator beside it — deliberately *not* a live-incrementing counter, since constantly ticking "1 minute ago" → "2 minutes ago" while a single toast is displayed would be more work for very little added believability; a fixed plausible time per event reads naturally without that complexity.

Dismiss means dismiss, for the whole session

Clicking the ✕ button doesn't just hide the current toast — it sets a dismissedManually flag that cycle() checks before showing anything, and clears the rotation interval outright. Once a user has explicitly said "stop showing me this," the pattern should respect that permanently rather than popping back up a few seconds later with the next event in the queue, which would undo the very trust the pattern is trying to build.

Where this pattern earns its keep, and where it backfires

Social proof works because specificity reads as truth — a named city and a named action feel real in a way "people love this!" never does. But the same specificity makes a fabricated or stale feed obvious the moment a visitor notices the same three names looping every visit, which is why real implementations should draw from an actually-updating event source rather than running a fixed demo list indefinitely in production.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAfter a short delay, a toast slides in near the bottom-left corner showing a purchase event with a colored avatar.
  2. 2
    Watch it auto-cycleEvery 8 seconds, the toast hides and a new event from the EVENTS queue appears in its place.
  3. 3
    Hover the toastA small ✕ dismiss button fades in at the top-right corner of the toast.
  4. 4
    Click dismissThe toast hides immediately and the rotation stops permanently for the rest of the session — it won't reappear with the next event.
  5. 5
    Edit the eventsChange any entry's name, action, avatar color, or mins value in the EVENTS array to customize the rotation content.
  6. 6
    Connect real activity dataReplace the static EVENTS array with recent orders/signups fetched from your backend, keeping the same showEvent()/cycle() rotation logic.

Real-world uses

Common Use Cases

E-commerce conversion optimization
The classic "recent purchase" notification pattern used by FOMO, Proof, and similar conversion tools.
SaaS landing pages
Show recent signups or upgrades to build trust before a visitor commits to creating an account.
Course and info-product sales pages
Display recent enrollments alongside a countdown timer or trial countdown for urgency plus social proof.
Marketplace and crowdfunding pages
Show recent backer or buyer activity to reinforce that a campaign or listing is actively gaining traction.
Event and ticket sales pages
Display recent ticket purchases to create urgency around a limited-availability event.
Learning respectful notification UX
A practical reference for an auto-cycling toast that still honors an explicit user dismissal — compare with a snackbar undo for action-based toast feedback.

Got questions?

Frequently Asked Questions

Fetch recent events from your backend (e.g. the last N orders or signups) on page load, map them into the { name, action, avatar, mins } shape EVENTS uses, and assign the result to EVENTS before the cycle() rotation starts — for fresher data, refetch periodically and merge new events into the queue.

An explicit dismiss is the strongest possible signal that a user doesn't want to see this pattern — popping the next event back up moments later would directly undo the trust the dismiss action was supposed to build. Respecting it for the whole session is the more honest choice, even though it costs you the remaining impressions.

In the close button's click handler, write a flag (and optionally a timestamp) to localStorage; on page load, check that flag before starting the cycle() rotation at all, and skip scheduling it entirely if the user dismissed it within your chosen cooldown window (e.g. the last 24 hours).

Store an actual createdAt timestamp per event instead of a fixed mins number, and compute the relative label ("3 minutes ago") at the moment showEvent() runs using a small time-formatting helper, so the displayed time reflects how long ago the event actually happened rather than a value baked into the data.

In React, keep the current event and visibility in useState and run the rotation interval inside useEffect with cleanup on unmount; in Vue, use ref()/onUnmounted for the same interval cleanup; in Angular, use a component field with ngOnDestroy. The dismissedManually flag and cycle logic are plain JavaScript and need no framework-specific changes.

Social Proof Popup — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Social Proof Popup</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh}
    
    .spp-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .spp-hint{color:#94a3b8;font-size:14px;font-weight:600;text-align:center;max-width:280px}
    
    .spp-toast{position:fixed;left:18px;bottom:18px;display:flex;align-items:center;gap:11px;background:#fff;border-radius:14px;
      padding:11px 14px;box-shadow:0 16px 40px rgba(15,23,42,.16);max-width:300px;z-index:90;
      transform:translateY(16px) scale(.97);opacity:0;pointer-events:none;transition:opacity .25s,transform .25s}
    .spp-toast.show{transform:translateY(0) scale(1);opacity:1;pointer-events:all}
    
    .spp-avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;background:#e2e8f0;object-fit:cover}
    .spp-body{display:flex;flex-direction:column;gap:1px;min-width:0}
    .spp-body strong{font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .spp-body span{font-size:12px;color:#64748b}
    .spp-time{font-size:10.5px;color:#22c55e;font-weight:700;display:flex;align-items:center;gap:4px}
    .spp-time::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e}
    
    .spp-close{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;border:none;background:#f1f5f9;color:#94a3b8;
      font-size:9px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s}
    .spp-toast:hover .spp-close{opacity:1}
    .spp-close:hover{background:#e2e8f0}
  </style>
</head>
<body>
  <div class="spp-page">
    <p class="spp-hint">A rotating purchase notification appears in the bottom-left corner.</p>
  </div>
  <div class="spp-toast" id="sppToast" role="status">
    <img class="spp-avatar" id="sppAvatar" alt="">
    <div class="spp-body">
      <strong id="sppName"></strong>
      <span id="sppAction"></span>
      <span class="spp-time" id="sppTime"></span>
    </div>
    <button type="button" class="spp-close" id="sppClose" aria-label="Dismiss">✕</button>
  </div>
  <script>
    var EVENTS = [
      { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
      { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
      { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
      { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
      { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
    ];
    
    var toast = document.getElementById('sppToast');
    var dismissedManually = false;
    var rotateIndex = 0;
    var cycleTimer = null;
    var hideTimer = null;
    
    function avatarUri(color) {
      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
      return 'data:image/svg+xml;base64,' + btoa(svg);
    }
    
    function showEvent(evt) {
      document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
      document.getElementById('sppName').textContent = evt.name;
      document.getElementById('sppAction').textContent = evt.action;
      document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
      toast.classList.add('show');
      clearTimeout(hideTimer);
      hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
    }
    
    function cycle() {
      if (dismissedManually) return;
      showEvent(EVENTS[rotateIndex % EVENTS.length]);
      rotateIndex++;
    }
    
    document.getElementById('sppClose').addEventListener('click', function () {
      dismissedManually = true;
      toast.classList.remove('show');
      clearTimeout(hideTimer);
      clearInterval(cycleTimer);
    });
    
    setTimeout(function () {
      cycle();
      cycleTimer = setInterval(cycle, 8000);
    }, 1500);
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Social Proof Popup</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh
    }

    .spp-toast.show {
      transform:translateY(0) scale(1);opacity:1;pointer-events:all
    }

    .spp-body strong {
      font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
    }

    .spp-body span {
      font-size:12px;color:#64748b
    }

    .spp-time::before {
      content:'';width:6px;height:6px;border-radius:50%;background:#22c55e
    }

    .spp-toast:hover .spp-close {
      opacity:1
    }
  </style>
</head>
<body>
  <div class="min-h-screen flex items-center justify-center p-6">
    <p class="text-[#94a3b8] text-sm font-semibold text-center max-w-[280px]">A rotating purchase notification appears in the bottom-left corner.</p>
  </div>
  <div class="spp-toast fixed left-[18px] bottom-[18px] flex items-center gap-[11px] bg-[#fff] rounded-[14px] py-[11px] px-3.5 shadow-[0_16px_40px_rgba(15,23,42,.16)] max-w-[300px] z-[90] [transform:translateY(16px)_scale(.97)] opacity-0 pointer-events-none [transition:opacity_.25s,transform_.25s]" id="sppToast" role="status">
    <img class="w-[38px] h-[38px] rounded-full shrink-0 bg-[#e2e8f0] object-cover" id="sppAvatar" alt="">
    <div class="spp-body flex flex-col gap-px min-w-0">
      <strong id="sppName"></strong>
      <span id="sppAction"></span>
      <span class="spp-time text-[10.5px] text-[#22c55e] font-bold flex items-center gap-1" id="sppTime"></span>
    </div>
    <button type="button" class="spp-close absolute top-1.5 right-1.5 w-[18px] h-[18px] rounded-full border-0 bg-[#f1f5f9] text-[#94a3b8] text-[9px] cursor-pointer flex items-center justify-center opacity-0 [transition:opacity_.15s] hover:bg-[#e2e8f0]" id="sppClose" aria-label="Dismiss">✕</button>
  </div>
  <script>
    var EVENTS = [
      { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
      { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
      { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
      { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
      { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
    ];
    
    var toast = document.getElementById('sppToast');
    var dismissedManually = false;
    var rotateIndex = 0;
    var cycleTimer = null;
    var hideTimer = null;
    
    function avatarUri(color) {
      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
      return 'data:image/svg+xml;base64,' + btoa(svg);
    }
    
    function showEvent(evt) {
      document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
      document.getElementById('sppName').textContent = evt.name;
      document.getElementById('sppAction').textContent = evt.action;
      document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
      toast.classList.add('show');
      clearTimeout(hideTimer);
      hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
    }
    
    function cycle() {
      if (dismissedManually) return;
      showEvent(EVENTS[rotateIndex % EVENTS.length]);
      rotateIndex++;
    }
    
    document.getElementById('sppClose').addEventListener('click', function () {
      dismissedManually = true;
      toast.classList.remove('show');
      clearTimeout(hideTimer);
      clearInterval(cycleTimer);
    });
    
    setTimeout(function () {
      cycle();
      cycleTimer = setInterval(cycle, 8000);
    }, 1500);
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to SocialProofPopup.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh}

.spp-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.spp-hint{color:#94a3b8;font-size:14px;font-weight:600;text-align:center;max-width:280px}

.spp-toast{position:fixed;left:18px;bottom:18px;display:flex;align-items:center;gap:11px;background:#fff;border-radius:14px;
  padding:11px 14px;box-shadow:0 16px 40px rgba(15,23,42,.16);max-width:300px;z-index:90;
  transform:translateY(16px) scale(.97);opacity:0;pointer-events:none;transition:opacity .25s,transform .25s}
.spp-toast.show{transform:translateY(0) scale(1);opacity:1;pointer-events:all}

.spp-avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;background:#e2e8f0;object-fit:cover}
.spp-body{display:flex;flex-direction:column;gap:1px;min-width:0}
.spp-body strong{font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.spp-body span{font-size:12px;color:#64748b}
.spp-time{font-size:10.5px;color:#22c55e;font-weight:700;display:flex;align-items:center;gap:4px}
.spp-time::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e}

.spp-close{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;border:none;background:#f1f5f9;color:#94a3b8;
  font-size:9px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s}
.spp-toast:hover .spp-close{opacity:1}
.spp-close:hover{background:#e2e8f0}
`;

export default function SocialProofPopup() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var EVENTS = [
      { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
      { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
      { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
      { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
      { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
    ];
    
    var toast = document.getElementById('sppToast');
    var dismissedManually = false;
    var rotateIndex = 0;
    var cycleTimer = null;
    var hideTimer = null;
    
    function avatarUri(color) {
      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
      return 'data:image/svg+xml;base64,' + btoa(svg);
    }
    
    function showEvent(evt) {
      document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
      document.getElementById('sppName').textContent = evt.name;
      document.getElementById('sppAction').textContent = evt.action;
      document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
      toast.classList.add('show');
      clearTimeout(hideTimer);
      hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
    }
    
    function cycle() {
      if (dismissedManually) return;
      showEvent(EVENTS[rotateIndex % EVENTS.length]);
      rotateIndex++;
    }
    
    document.getElementById('sppClose').addEventListener('click', function () {
      dismissedManually = true;
      toast.classList.remove('show');
      clearTimeout(hideTimer);
      clearInterval(cycleTimer);
    });
    
    setTimeout(function () {
      cycle();
      cycleTimer = setInterval(cycle, 8000);
    }, 1500);
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="spp-page">
        <p className="spp-hint">A rotating purchase notification appears in the bottom-left corner.</p>
      </div>
      <div className="spp-toast" id="sppToast" role="status">
        <img className="spp-avatar" id="sppAvatar" alt="" />
        <div className="spp-body">
          <strong id="sppName"></strong>
          <span id="sppAction"></span>
          <span className="spp-time" id="sppTime"></span>
        </div>
        <button type="button" className="spp-close" id="sppClose" aria-label="Dismiss">✕</button>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function SocialProofPopup() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var EVENTS = [
      { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
      { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
      { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
      { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
      { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
    ];
    
    var toast = document.getElementById('sppToast');
    var dismissedManually = false;
    var rotateIndex = 0;
    var cycleTimer = null;
    var hideTimer = null;
    
    function avatarUri(color) {
      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
      return 'data:image/svg+xml;base64,' + btoa(svg);
    }
    
    function showEvent(evt) {
      document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
      document.getElementById('sppName').textContent = evt.name;
      document.getElementById('sppAction').textContent = evt.action;
      document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
      toast.classList.add('show');
      clearTimeout(hideTimer);
      hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
    }
    
    function cycle() {
      if (dismissedManually) return;
      showEvent(EVENTS[rotateIndex % EVENTS.length]);
      rotateIndex++;
    }
    
    document.getElementById('sppClose').addEventListener('click', function () {
      dismissedManually = true;
      toast.classList.remove('show');
      clearTimeout(hideTimer);
      clearInterval(cycleTimer);
    });
    
    setTimeout(function () {
      cycle();
      cycleTimer = setInterval(cycle, 8000);
    }, 1500);
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh
}

.spp-toast.show {
  transform:translateY(0) scale(1);opacity:1;pointer-events:all
}

.spp-body strong {
  font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
}

.spp-body span {
  font-size:12px;color:#64748b
}

.spp-time::before {
  content:'';width:6px;height:6px;border-radius:50%;background:#22c55e
}

.spp-toast:hover .spp-close {
  opacity:1
}
      `}</style>
      <div className="min-h-screen flex items-center justify-center p-6">
        <p className="text-[#94a3b8] text-sm font-semibold text-center max-w-[280px]">A rotating purchase notification appears in the bottom-left corner.</p>
      </div>
      <div className="spp-toast fixed left-[18px] bottom-[18px] flex items-center gap-[11px] bg-[#fff] rounded-[14px] py-[11px] px-3.5 shadow-[0_16px_40px_rgba(15,23,42,.16)] max-w-[300px] z-[90] [transform:translateY(16px)_scale(.97)] opacity-0 pointer-events-none [transition:opacity_.25s,transform_.25s]" id="sppToast" role="status">
        <img className="w-[38px] h-[38px] rounded-full shrink-0 bg-[#e2e8f0] object-cover" id="sppAvatar" alt="" />
        <div className="spp-body flex flex-col gap-px min-w-0">
          <strong id="sppName"></strong>
          <span id="sppAction"></span>
          <span className="spp-time text-[10.5px] text-[#22c55e] font-bold flex items-center gap-1" id="sppTime"></span>
        </div>
        <button type="button" className="spp-close absolute top-1.5 right-1.5 w-[18px] h-[18px] rounded-full border-0 bg-[#f1f5f9] text-[#94a3b8] text-[9px] cursor-pointer flex items-center justify-center opacity-0 [transition:opacity_.15s] hover:bg-[#e2e8f0]" id="sppClose" aria-label="Dismiss">✕</button>
      </div>
    </>
  );
}
Vue
<template>
  <div class="spp-page">
    <p class="spp-hint">A rotating purchase notification appears in the bottom-left corner.</p>
  </div>
  <div class="spp-toast" id="sppToast" role="status">
    <img class="spp-avatar" id="sppAvatar" alt="">
    <div class="spp-body">
      <strong id="sppName"></strong>
      <span id="sppAction"></span>
      <span class="spp-time" id="sppTime"></span>
    </div>
    <button type="button" class="spp-close" id="sppClose" aria-label="Dismiss">✕</button>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

var EVENTS = [
  { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
  { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
  { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
  { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
  { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
];
let toast;
var dismissedManually = false;
var rotateIndex = 0;
var cycleTimer = null;
var hideTimer = null;
function avatarUri(color) {
  var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
  return 'data:image/svg+xml;base64,' + btoa(svg);
}
function showEvent(evt) {
  document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
  document.getElementById('sppName').textContent = evt.name;
  document.getElementById('sppAction').textContent = evt.action;
  document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
  toast.classList.add('show');
  clearTimeout(hideTimer);
  hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
}
function cycle() {
  if (dismissedManually) return;
  showEvent(EVENTS[rotateIndex % EVENTS.length]);
  rotateIndex++;
}

onMounted(() => {
  toast = document.getElementById('sppToast');
  document.getElementById('sppClose').addEventListener('click', function () {
    dismissedManually = true;
    toast.classList.remove('show');
    clearTimeout(hideTimer);
    clearInterval(cycleTimer);
  });
  setTimeout(function () {
    cycle();
    cycleTimer = setInterval(cycle, 8000);
  }, 1500);
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh}

.spp-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.spp-hint{color:#94a3b8;font-size:14px;font-weight:600;text-align:center;max-width:280px}

.spp-toast{position:fixed;left:18px;bottom:18px;display:flex;align-items:center;gap:11px;background:#fff;border-radius:14px;
  padding:11px 14px;box-shadow:0 16px 40px rgba(15,23,42,.16);max-width:300px;z-index:90;
  transform:translateY(16px) scale(.97);opacity:0;pointer-events:none;transition:opacity .25s,transform .25s}
.spp-toast.show{transform:translateY(0) scale(1);opacity:1;pointer-events:all}

.spp-avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;background:#e2e8f0;object-fit:cover}
.spp-body{display:flex;flex-direction:column;gap:1px;min-width:0}
.spp-body strong{font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.spp-body span{font-size:12px;color:#64748b}
.spp-time{font-size:10.5px;color:#22c55e;font-weight:700;display:flex;align-items:center;gap:4px}
.spp-time::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e}

.spp-close{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;border:none;background:#f1f5f9;color:#94a3b8;
  font-size:9px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s}
.spp-toast:hover .spp-close{opacity:1}
.spp-close:hover{background:#e2e8f0}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-social-proof-popup',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="spp-page">
      <p class="spp-hint">A rotating purchase notification appears in the bottom-left corner.</p>
    </div>
    <div class="spp-toast" id="sppToast" role="status">
      <img class="spp-avatar" id="sppAvatar" alt="">
      <div class="spp-body">
        <strong id="sppName"></strong>
        <span id="sppAction"></span>
        <span class="spp-time" id="sppTime"></span>
      </div>
      <button type="button" class="spp-close" id="sppClose" aria-label="Dismiss">✕</button>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh}
    
    .spp-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .spp-hint{color:#94a3b8;font-size:14px;font-weight:600;text-align:center;max-width:280px}
    
    .spp-toast{position:fixed;left:18px;bottom:18px;display:flex;align-items:center;gap:11px;background:#fff;border-radius:14px;
      padding:11px 14px;box-shadow:0 16px 40px rgba(15,23,42,.16);max-width:300px;z-index:90;
      transform:translateY(16px) scale(.97);opacity:0;pointer-events:none;transition:opacity .25s,transform .25s}
    .spp-toast.show{transform:translateY(0) scale(1);opacity:1;pointer-events:all}
    
    .spp-avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;background:#e2e8f0;object-fit:cover}
    .spp-body{display:flex;flex-direction:column;gap:1px;min-width:0}
    .spp-body strong{font-size:13px;font-weight:800;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .spp-body span{font-size:12px;color:#64748b}
    .spp-time{font-size:10.5px;color:#22c55e;font-weight:700;display:flex;align-items:center;gap:4px}
    .spp-time::before{content:'';width:6px;height:6px;border-radius:50%;background:#22c55e}
    
    .spp-close{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;border:none;background:#f1f5f9;color:#94a3b8;
      font-size:9px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s}
    .spp-toast:hover .spp-close{opacity:1}
    .spp-close:hover{background:#e2e8f0}
  `]
})
export class SocialProofPopupComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var EVENTS = [
      { name: 'Sarah from Austin, TX', action: 'just purchased the Pro plan', avatar: '#6366f1', mins: 2 },
      { name: 'Devon from Toronto, ON', action: 'just signed up', avatar: '#22c55e', mins: 4 },
      { name: 'Mei from Singapore', action: 'just upgraded to Team', avatar: '#f59e0b', mins: 6 },
      { name: 'Lucas from São Paulo', action: 'just left a 5-star review', avatar: '#ec4899', mins: 9 },
      { name: 'Anna from Berlin', action: 'just purchased the Pro plan', avatar: '#0ea5e9', mins: 12 },
    ];
    
    var toast = document.getElementById('sppToast');
    var dismissedManually = false;
    var rotateIndex = 0;
    var cycleTimer = null;
    var hideTimer = null;
    
    function avatarUri(color) {
      var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38"><rect width="38" height="38" fill="' + color + '"/></svg>';
      return 'data:image/svg+xml;base64,' + btoa(svg);
    }
    
    function showEvent(evt) {
      document.getElementById('sppAvatar').src = avatarUri(evt.avatar);
      document.getElementById('sppName').textContent = evt.name;
      document.getElementById('sppAction').textContent = evt.action;
      document.getElementById('sppTime').textContent = evt.mins + ' minutes ago';
      toast.classList.add('show');
      clearTimeout(hideTimer);
      hideTimer = setTimeout(function () { toast.classList.remove('show'); }, 5200);
    }
    
    function cycle() {
      if (dismissedManually) return;
      showEvent(EVENTS[rotateIndex % EVENTS.length]);
      rotateIndex++;
    }
    
    document.getElementById('sppClose').addEventListener('click', function () {
      dismissedManually = true;
      toast.classList.remove('show');
      clearTimeout(hideTimer);
      clearInterval(cycleTimer);
    });
    
    setTimeout(function () {
      cycle();
      cycleTimer = setInterval(cycle, 8000);
    }, 1500);

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { avatarUri, showEvent, cycle });
  }
}