More Modals Snippets
Social Proof Popup — Rotating Purchase Notification
Social Proof Popup · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Social Proof Popup — Auto-Cycling Purchase Toasts with a Persistent Dismiss

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
- 1Paste HTML, CSS, and JSAfter a short delay, a toast slides in near the bottom-left corner showing a purchase event with a colored avatar.
- 2Watch it auto-cycleEvery 8 seconds, the toast hides and a new event from the EVENTS queue appears in its place.
- 3Hover the toastA small ✕ dismiss button fades in at the top-right corner of the toast.
- 4Click dismissThe toast hides immediately and the rotation stops permanently for the rest of the session — it won't reappear with the next event.
- 5Edit the eventsChange any entry's name, action, avatar color, or mins value in the EVENTS array to customize the rotation content.
- 6Connect 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
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
<!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><!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>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>
</>
);
}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>
</>
);
}<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>// @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 });
}
}