Free Recent Sales Popup Widget
Social-proof toast that cycles recent purchases in the corner of your store.
A small "someone just bought" notification that pops up in the bottom-left corner and cycles through a list of recent orders. Built for e-commerce stores that want lightweight social proof without a SaaS subscription. Paste one script tag with your sales list and it runs anywhere.
Embed this widget
Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.
<script src="https://hayneslab.dev/widgets/recent-sales-popup.js" data-hl-config='[{"name":"Emma","city":"Charlotte, NC","product":"Linen Throw Pillow","minutes":4},{"name":"Liam","city":"Austin, TX","product":"Ceramic Pour-Over Set","minutes":11},{"name":"Sofia","city":"Denver, CO","product":"Walnut Desk Organizer","minutes":19},{"name":"Noah","city":"Portland, OR","product":"Soy Candle Trio","minutes":27},{"name":"Ava","city":"Nashville, TN","product":"Leather Weekender Bag","minutes":38}]' data-hl-interval="6000" async></script>
<!-- Free Recent Sales Popup widget by haynes lab — https://hayneslab.dev/tools/widgets/recent-sales-popup --><!-- Free Recent Sales Popup widget by haynes lab — https://hayneslab.dev/tools/widgets/recent-sales-popup -->
<script data-hl-config='[{"name":"Emma","city":"Charlotte, NC","product":"Linen Throw Pillow","minutes":4},{"name":"Liam","city":"Austin, TX","product":"Ceramic Pour-Over Set","minutes":11},{"name":"Sofia","city":"Denver, CO","product":"Walnut Desk Organizer","minutes":19},{"name":"Noah","city":"Portland, OR","product":"Soy Candle Trio","minutes":27},{"name":"Ava","city":"Nashville, TN","product":"Leather Weekender Bag","minutes":38}]' data-hl-interval="6000">
/*!
* haynes lab — Recent Sales Popup widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/recent-sales-popup
*/
(function () {
'use strict';
var PREFIX = 'hlw-recent-sales-popup';
var STYLE_ID = 'hlw-recent-sales-popup-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/recent-sales-popup.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { position: fixed; left: 16px; bottom: 16px; z-index: 99999;' +
' width: min(340px, calc(100vw - 32px));' +
' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
' font-size: 14px; line-height: 1.45; color: #1f2937;' +
' opacity: 0; transform: translateY(12px); pointer-events: none;' +
' transition: opacity 0.35s ease, transform 0.35s ease; }' +
'.' + PREFIX + '--stage { position: absolute; left: 12px; bottom: 12px; z-index: 10; }' +
'.' + PREFIX + '--visible { opacity: 1; transform: translateY(0); pointer-events: auto; }' +
'.' + PREFIX + '__card { position: relative; display: flex; align-items: center; gap: 12px;' +
' background: #ffffff; border: 1px solid #e5e7eb; border-radius: 12px;' +
' padding: 12px 36px 12px 12px; box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12); }' +
'.' + PREFIX + '__icon { flex: 0 0 40px; width: 40px; height: 40px; border-radius: 8px;' +
' background: #fff7ed; display: flex; align-items: center; justify-content: center;' +
' font-size: 20px; line-height: 1; }' +
'.' + PREFIX + '__body { min-width: 0; }' +
'.' + PREFIX + '__text { margin: 0; color: #111827; }' +
'.' + PREFIX + '__text strong { font-weight: 600; }' +
'.' + PREFIX + '__time { margin: 2px 0 0; font-size: 12px; color: #6b7280; }' +
'.' + PREFIX + '__close { position: absolute; top: 6px; right: 6px; width: 22px; height: 22px;' +
' border: 0; border-radius: 6px; background: transparent; color: #9ca3af; font-size: 14px;' +
' line-height: 1; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; }' +
'.' + PREFIX + '__close:hover { background: #f3f4f6; color: #4b5563; }' +
'.' + PREFIX + '__close:focus-visible { outline: 2px solid #f97316; outline-offset: 1px; }' +
'.' + PREFIX + '__credit { display: inline-flex; align-items: center; gap: 4px; margin-top: 6px;' +
' font-size: 11px; color: #9ca3af; text-decoration: none;' +
' text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); }' +
'.' + PREFIX + '__credit svg { display: block; }' +
'.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
function parseSales(script) {
var raw = script.getAttribute('data-hl-config');
if (!raw) {
console.warn('[haynes lab recent-sales-popup] Missing required data-hl-config attribute (JSON array of {name, city, product, minutes}). Nothing rendered.');
return null;
}
var items;
try {
items = JSON.parse(raw);
} catch (e) {
console.warn('[haynes lab recent-sales-popup] data-hl-config is not valid JSON. Nothing rendered.', e);
return null;
}
if (!Array.isArray(items)) {
console.warn('[haynes lab recent-sales-popup] data-hl-config must be a JSON array. Nothing rendered.');
return null;
}
var valid = items.filter(function (s) {
return s && typeof s.name === 'string' && s.name &&
typeof s.city === 'string' && s.city &&
typeof s.product === 'string' && s.product;
});
if (!valid.length) {
console.warn('[haynes lab recent-sales-popup] data-hl-config has no valid entries (each needs "name", "city", and "product"). Nothing rendered.');
return null;
}
return valid;
}
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = text;
return node;
}
function logoSvg() {
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('viewBox', '0 0 32 32');
svg.setAttribute('width', '13');
svg.setAttribute('height', '13');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
function circle(cx, cy, r, attrs) {
var c = document.createElementNS(svgNS, 'circle');
c.setAttribute('cx', cx);
c.setAttribute('cy', cy);
c.setAttribute('r', r);
for (var key in attrs) c.setAttribute(key, attrs[key]);
svg.appendChild(c);
}
circle(16, 16, 16, { fill: '#0f0a0a' });
circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
circle(16, 16, 3, { fill: '#06b6d4' });
circle(24.5, 7.5, 2, { fill: '#f97316' });
circle(7, 16, 1.8, { fill: '#f97316' });
circle(20, 20, 1.5, { fill: '#f97316' });
return svg;
}
function timeAgo(minutes) {
var m = parseInt(minutes, 10);
if (isNaN(m) || m < 1) return 'just now';
if (m === 1) return '1 minute ago';
if (m < 60) return m + ' minutes ago';
var h = Math.floor(m / 60);
return h === 1 ? '1 hour ago' : h + ' hours ago';
}
function build(script) {
var sales = parseSales(script);
if (!sales) return;
var interval = parseInt(script.getAttribute('data-hl-interval'), 10);
if (isNaN(interval) || interval < 4000) interval = 8000;
var inStage = !!script.closest('.widget-demo__stage');
injectStyles();
var root = el('div', PREFIX + (inStage ? ' ' + PREFIX + '--stage' : ''));
root.setAttribute('role', 'status');
root.setAttribute('aria-live', 'polite');
var card = el('div', PREFIX + '__card');
card.appendChild(el('div', PREFIX + '__icon', '🛍️'));
var body = el('div', PREFIX + '__body');
card.appendChild(body);
var close = el('button', PREFIX + '__close', '✕');
close.type = 'button';
close.setAttribute('aria-label', 'Dismiss recent sales notifications');
card.appendChild(close);
root.appendChild(card);
var credit = el('a', PREFIX + '__credit');
credit.appendChild(logoSvg());
credit.appendChild(document.createTextNode('Powered by haynes lab'));
credit.href = 'https://hayneslab.dev/tools/widgets/recent-sales-popup';
credit.target = '_blank';
credit.rel = 'noopener';
root.appendChild(credit);
var index = 0;
var timer = null;
var dismissed = false;
function show() {
if (dismissed) return;
var s = sales[index % sales.length];
index++;
body.textContent = '';
var text = el('p', PREFIX + '__text');
var strong = el('strong', null, s.name + ' from ' + s.city);
text.appendChild(strong);
text.appendChild(document.createTextNode(' bought ' + s.product));
body.appendChild(text);
body.appendChild(el('p', PREFIX + '__time', timeAgo(s.minutes)));
root.classList.add(PREFIX + '--visible');
timer = setTimeout(hide, 5000);
}
function hide() {
root.classList.remove(PREFIX + '--visible');
if (!dismissed) timer = setTimeout(show, interval);
}
close.addEventListener('click', function () {
dismissed = true;
if (timer) clearTimeout(timer);
root.classList.remove(PREFIX + '--visible');
});
if (inStage) {
script.closest('.widget-demo__stage').appendChild(root);
} else {
document.body.appendChild(root);
}
timer = setTimeout(show, 1500);
}
function init() {
var script = findScript();
if (!script) {
console.warn('[haynes lab recent-sales-popup] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>Configuration
Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.
| Attribute | Default | Description |
|---|---|---|
data-hl-config | (required) | JSON array of sales, each with "name", "city", "product", and optional "minutes" (how long ago the purchase happened). |
data-hl-interval | "8000" | Milliseconds between popups after each one hides (minimum 4000). |
Frequently asked questions
How do I install the recent sales popup?
Paste the script tag anywhere before the closing </body> tag with your sales list in data-hl-config. The popup appears fixed in the bottom-left corner and cycles automatically.
Can I control how often the popup appears?
Yes — set data-hl-interval to the number of milliseconds between popups (default 8000, minimum 4000). Each popup stays visible for 5 seconds before hiding.
Does it work on WordPress, Shopify, or Squarespace?
Yes — it is a single vanilla JS file with no dependencies, so it works on any platform that lets you add a custom script tag, including WordPress, Shopify, Squarespace, and plain HTML sites.
More free widgets
Star Rating Badge
Show your aggregate rating with stars, a score, and a review count.
View toolE-commerce & RetailPromo Code Reveal
A click-to-reveal coupon that shows and copies your promo code.
View toolE-commerce & RetailFree Shipping Progress
Show shoppers exactly how close they are to unlocking free shipping.
View toolNeed a custom widget?
We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.
Get in Touch