
Social Proof Widgets
- 67 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Display real-time social proof like recent purchases, review counts, visitor counts, and verified-buyer badges to build trust.
About
Shows real-time social-proof widgets including recent purchases, review and visitor counts, and verified-buyer badges. A developer uses it to boost trust and conversion on product pages.
- Recent-purchase and live visitor-count widgets
- Verified-buyer badges and review counts
Social Proof Widgets by the numbers
- 67 all-time installs (skills.sh)
- Ranked #519 of 853 Sales & Marketing skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill social-proof-widgetsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Display real-time social proof like recent purchases, review counts, visitor counts, and verified-buyer badges to build trust.
Files
Social Proof Widgets
Overview
Social proof widgets — recent purchase notifications, visitor counts, review badges, and low-stock indicators — reduce purchase anxiety and increase conversion rates by 10–30% on product pages. For Shopify and WooCommerce, dedicated apps (Fomo, TrustPulse, ProveSource) install these widgets without code. Building custom widgets is only necessary for headless stores with specific design requirements.
When to Use This Skill
- When product pages have good traffic but low conversion rates
- When launching a new product that lacks reviews and needs other trust signals
- When testing whether social proof elements meaningfully impact CVR (A/B test required)
- When wanting to add real-time purchase notifications
- When building a low-stock urgency display based on actual inventory data
Core Instructions
Step 1: Choose the right social proof tool
| Platform | Best For | Shopify | WooCommerce | BigCommerce | Price |
|---|---|---|---|---|---|
| Fomo | Real-time purchase notifications | App Store | Plugin | Via JS | $19+/mo |
| TrustPulse | Recent purchases + live visitor count | — | Plugin | Via JS | $5+/mo |
| ProveSource | All platforms, highly customizable | App Store | Via JS | Via JS | Free tier; $20+/mo |
| Judge.me | Review count badge + verified buyer badge | App Store | Plugin | App Marketplace | Free tier; $15/mo |
| Custom | Headless stores, specific design needs | Via API | Via API | Via API | Dev cost |
Recommendation: Use Fomo for Shopify (best purchase notifications) and TrustPulse for WooCommerce. For review badges, use your existing review platform (Judge.me, Yotpo) — they include star rating badges automatically.
Step 2: Set up social proof widgets
---
Shopify with Fomo
1. Install Fomo from the Shopify App Store 2. Go to Fomo → Events → Shopify Orders — Fomo automatically imports recent orders from your store 3. Configure the notification template:
- Display: first name and city (e.g., "Sarah from Chicago just purchased…")
- Show the product name and image
- Display within the last 48 hours
4. Go to Fomo → Design to customize the position and style of the notification toast (bottom-left by default) 5. Go to Fomo → Rules and set:
- Show only on product pages (URL contains
/products/) - Hide on cart and checkout pages
- Minimum 3 notifications to show before displaying the widget (prevents showing a widget with only 1 event)
6. Go to Fomo → A/B Testing to split-test the widget against a control group — always measure lift before enabling sitewide
---
WooCommerce with TrustPulse
1. Install TrustPulse from the WordPress plugin directory 2. Go to TrustPulse → Create Campaign and select Recent Activity 3. Configure:
- Data source: WooCommerce Recent Orders
- Display: customer name, city, product purchased
- Show: orders from the last 7 days
4. Set targeting rules: show only on product and shop pages; exclude checkout 5. For the visitor count widget, create a second On Fire campaign: "X people looking at this right now" — set a minimum visitor threshold of 5 before displaying
---
BigCommerce with ProveSource
1. Sign up for ProveSource and add the JavaScript snippet to your store via Storefront → Script Manager 2. Connect your BigCommerce store to ProveSource via the integration settings 3. Configure recent purchase notifications and visitor count widgets from the ProveSource dashboard
---
Custom / Headless
For headless stores, build a social proof API and client widget:
// GET /api/products/:id/social-proof
// Returns real data only — never fabricate counts
export async function getProductSocialProof(req: Request, res: Response) {
const productId = req.params.id;
const [recentOrders, reviewSummary, stockLevel] = await Promise.all([
db.orderLineItems.findAll({
where: { productId, createdAt: { gte: subHours(new Date(), 48) } },
include: ['order.shippingAddress'],
limit: 10,
}),
db.productReviews.aggregate(productId),
db.productVariants.findMinStock(productId),
]);
// Anonymize PII — first name and city only
const recentPurchases = recentOrders.map(item => ({
firstName: item.order.shippingAddress.firstName,
location: `${item.order.shippingAddress.city}, ${item.order.shippingAddress.stateCode}`,
timeAgo: formatTimeAgo(item.createdAt),
productName: item.productName,
}));
return res.json({
recentPurchases,
reviews: { average: reviewSummary.avgRating, total: reviewSummary.total },
stockLevel: {
isLow: stockLevel > 0 && stockLevel <= 5,
quantity: stockLevel,
isSoldOut: stockLevel === 0,
},
});
}Client-side purchase notification toast — using DOM methods to prevent XSS:
class SocialProofToast {
private queue: Array<{ firstName: string; location: string; productName: string; timeAgo: string }> = [];
private isShowing = false;
async init(productId: string) {
const response = await fetch(`/api/products/${productId}/social-proof`);
const data = await response.json();
this.queue = data.recentPurchases.slice(0, 5);
this.showNext();
}
private showNext() {
if (this.queue.length === 0 || this.isShowing) return;
const purchase = this.queue.shift()!;
this.isShowing = true;
const toast = document.createElement('div');
toast.className = 'sp-toast';
const strong = document.createElement('strong');
strong.textContent = `${purchase.firstName} from ${purchase.location}`; // textContent prevents XSS
const span = document.createElement('span');
span.textContent = ` purchased ${purchase.productName}`;
const time = document.createElement('time');
time.textContent = purchase.timeAgo;
toast.appendChild(strong);
toast.appendChild(span);
toast.appendChild(time);
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
this.isShowing = false;
setTimeout(() => this.showNext(), 8000); // 8-second gap between toasts
}, 5000);
}
}Step 3: Add low-stock urgency indicators
Low-stock messaging ("Only 3 left!") drives urgency effectively — but only show real inventory counts. Fake urgency destroys trust when customers notice it.
In Shopify: Install Urgency Bear or Hurrify from the Shopify App Store — they read your actual Shopify inventory and display "Only X left" messages on product pages.
In WooCommerce: Enable WooCommerce's built-in low-stock display: 1. Go to WooCommerce → Settings → Products → Inventory 2. Enable Show stock management at product level 3. Enable Enable low stock threshold and set to 5 4. WooCommerce automatically shows "Only 3 in stock" on product pages when inventory falls below the threshold
Custom thresholds: Use CSS to style the low-stock message based on quantity levels.
Step 4: A/B test social proof impact
Always test before deploying sitewide. Both Fomo and TrustPulse have built-in A/B testing.
For manual A/B testing: 1. Enable the widget for 50% of visitors using your platform's experimentation tool 2. Measure for at least 2 weeks and 200+ conversions per group 3. Compare conversion rate, AOV, and revenue per visitor between the groups 4. Only keep the widget if it shows statistically significant lift (p < 0.05)
Best Practices
- Only show real data — fabricated purchase counts or invented visitor numbers erode trust when discovered; use a minimum threshold (5+ events) before showing any widget
- Anonymize purchase notifications — show first name and city only; never include order IDs, full names, or email addresses
- Load social proof asynchronously — fetch after page load using
requestIdleCallback; never block the critical render path - Hide the widget on cart and checkout pages — showing purchase notifications during checkout distracts from conversion; disable on these pages
- Cap notification frequency — show a maximum of 3 toasts per page session with 8+ second gaps between them
- Use `textContent` for all customer-derived data — prevents XSS vulnerabilities; never use
innerHTMLwith customer names or locations
Common Pitfalls
| Problem | Solution |
|---|---|
| Toast notifications feel spammy | Limit to 3 per session; add 8-second gaps; hide on cart/checkout |
| Widget hurts conversion (A/B test shows negative lift) | Disable immediately; test with different placement, copy, or timing before abandoning |
| Review badge not appearing in Google search results | Ensure AggregateRating schema is server-rendered (not just client-side JS); verify with Google Rich Results Test |
| Low-stock indicator showing wrong quantity | Subscribe to inventory change webhooks rather than polling; stale data creates false urgency |
| Social proof widget slowing page load | Load all social proof widgets asynchronously after page interactive; use requestIdleCallback or setTimeout(fn, 0) |
Related Skills
- @review-generation-engine
- @conversion-rate-optimization
- @exit-intent-popups
- @ugc-campaign-management
- @ab-testing-ecommerce
{
"context": "Tests whether the agent implements the purchase notification toast widget following the skill's DOM safety, timing, session cap, page restriction, and queue size rules.",
"type": "weighted_checklist",
"checklist": [
{
"name": "DOM createElement construction",
"max_score": 12,
"description": "Toast DOM nodes are built using document.createElement (not innerHTML or insertAdjacentHTML) for elements that display customer data"
},
{
"name": "textContent for customer data",
"max_score": 12,
"description": "Customer-derived strings (firstName, location, variantTitle) are assigned via .textContent — not via innerHTML, template literals injected into innerHTML, or string concatenation into innerHTML"
},
{
"name": "Auto-dismiss at 5000ms",
"max_score": 8,
"description": "Each toast is automatically dismissed after 5000ms (5 seconds)"
},
{
"name": "Gap between toasts 8000ms",
"max_score": 8,
"description": "There is an 8000ms (8 second) delay between showing consecutive toasts"
},
{
"name": "Max 3 toasts per session",
"max_score": 10,
"description": "The widget shows a maximum of 3 toasts per session (or widget-notes.md states 3 as the per-session cap)"
},
{
"name": "Not on cart/checkout",
"max_score": 8,
"description": "The code or widget-notes.md explicitly states the widget should NOT appear on cart or checkout pages"
},
{
"name": "Queue capped at 6",
"max_score": 8,
"description": "The toast queue is initialised with at most 6 items from the recentPurchases array (e.g. .slice(0, 6))"
},
{
"name": "data-product-id attribute",
"max_score": 8,
"description": "The product ID is read from a [data-product-id] DOM attribute (via querySelector or getAttribute), not from a hardcoded value or a different mechanism"
},
{
"name": "Conditional initialisation",
"max_score": 8,
"description": "The toast widget only initialises when a [data-product-id] element is present on the page"
},
{
"name": "Close button",
"max_score": 6,
"description": "Each toast includes a dismiss/close button that removes the toast when clicked"
},
{
"name": "timeAgo displayed",
"max_score": 5,
"description": "The toast displays the timeAgo value from the purchase data (how long ago the purchase was made)"
},
{
"name": "Widget notes completeness",
"max_score": 7,
"description": "widget-notes.md documents: per-session cap (3), gap between toasts (8000ms or 8s), auto-dismiss duration (5000ms or 5s), and the XSS prevention approach (textContent / DOM methods)"
}
]
}
Purchase Notification Toast Widget for a Storefront
Problem/Feature Description
A mid-size e-commerce brand wants to add real-time purchase notification toasts to their product pages — small pop-ups showing messages like "Alex from Portland just bought this in Blue / Large". They want these built in-house rather than relying on a paid SaaS tool, and they have a working backend endpoint already deployed at /api/products/:id/social-proof that returns a recentPurchases array (each entry has firstName, location, variantTitle, and timeAgo).
The site already injects a data-product-id attribute on a DOM element on product pages to identify which product is being viewed. The engineering team is concerned about security and user experience: the widget must handle customer-supplied data safely, and the notification cadence should not feel spammy. The widget should only appear on product pages, not on the cart or checkout.
Output Specification
Produce a self-contained TypeScript file toast-widget.ts that implements the purchase notification toast widget as a class. The class should:
- Fetch notifications from the social proof endpoint
- Display toasts one at a time from a queue
- Handle dismissal and automatic cycling
Also produce a short widget-notes.md documenting:
- How many toasts are shown per session before stopping
- The timing between consecutive toasts (in milliseconds or seconds)
- How long each individual toast remains visible before auto-dismissing
- How the widget avoids XSS when rendering customer names and locations
{
"context": "Tests whether the agent implements the review badge with correct schema.org structured data and server-rendering rationale, returns empty for zero reviews, builds stars correctly, and implements the stock indicator with the exact numeric thresholds from the skill.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Empty string for zero reviews",
"max_score": 8,
"description": "renderReviewBadge returns an empty string (or equivalent no-output) when reviewCount === 0"
},
{
"name": "Stars with Math.round",
"max_score": 8,
"description": "The number of filled stars is determined by Math.round(avgRating), not Math.floor or Math.ceil"
},
{
"name": "5 star spans",
"max_score": 6,
"description": "The star display renders exactly 5 star elements (spans or equivalent), with filled vs empty distinction"
},
{
"name": "schema.org AggregateRating itemscope",
"max_score": 10,
"description": "The review badge HTML includes itemscope and itemtype=\"https://schema.org/AggregateRating\""
},
{
"name": "itemprop reviewCount",
"max_score": 8,
"description": "The review count element uses itemprop=\"reviewCount\""
},
{
"name": "itemprop ratingValue",
"max_score": 8,
"description": "The average rating is marked up with itemprop=\"ratingValue\" (via a meta tag or element attribute)"
},
{
"name": "bestRating=5",
"max_score": 6,
"description": "itemprop=\"bestRating\" with content=\"5\" is included in the badge markup"
},
{
"name": "Server-side rationale",
"max_score": 8,
"description": "widget-spec.md explains that the review badge should be server-rendered (not client-side only) for Google/search-engine indexing or rich snippet eligibility"
},
{
"name": "Sold-out tier",
"max_score": 5,
"description": "renderStockIndicator returns an out-of-stock message when stockLevel === 0"
},
{
"name": "Critical tier threshold ≤3",
"max_score": 8,
"description": "renderStockIndicator shows a 'only N left' or urgency message for stockLevel values 1, 2, and 3 (the critical tier cutoff is exactly 3)"
},
{
"name": "Low tier threshold ≤10",
"max_score": 8,
"description": "renderStockIndicator shows a 'low stock' message for stockLevel values 4–10 (the low tier cutoff is exactly 10)"
},
{
"name": "Empty above threshold",
"max_score": 6,
"description": "renderStockIndicator returns an empty string (or no output) when stockLevel > 10"
},
{
"name": "Numeric interpolation only",
"max_score": 5,
"description": "The stock indicator HTML only interpolates the numeric stockLevel value — no user-input strings are interpolated into the markup"
},
{
"name": "Visitor threshold noted",
"max_score": 6,
"description": "widget-spec.md or trust-widgets.ts references or documents that a visitor counter should only be displayed when the count exceeds a minimum threshold (e.g. 5)"
}
]
}
Product Page Trust Widgets: Review Badge and Stock Urgency Indicator
Problem/Feature Description
A retailer's SEO team has noticed that competitor product pages appear with star ratings in Google search results (rich snippets), while theirs do not — costing them click-through rate. At the same time, their UX team wants to add a stock urgency indicator to create honest purchase urgency for products that are genuinely running low. Both of these features need to be added to the server-side template rendering layer so they are available on first load.
The backend already has a product object available during server-side rendering with avgRating (a number 0–5) and reviewCount (integer). For the stock indicator, a stockLevel integer (quantity on hand) is available. The team wants the review badge to contribute to SEO and the stock indicator to reflect real inventory tiers accurately.
Output Specification
Produce a TypeScript file trust-widgets.ts containing:
- A
renderReviewBadge(product)function that returns an HTML string - A
renderStockIndicator(stockLevel: number)function that returns an HTML string
Also produce a widget-spec.md that documents:
- The exact stock level thresholds used and the message shown for each tier
- What happens when a product has zero reviews
- Why the review badge HTML should be rendered on the server rather than the client
{
"context": "Tests whether the agent builds the social proof API endpoint following the skill's specific patterns: a unified endpoint using parallel DB/Redis queries, correct PII anonymization, Redis sorted-set visitor tracking with the right window and key expiry, the correct isLow threshold, the Math.max floor for activeVisitors, and the random boost for very low counts.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Unified endpoint path",
"max_score": 5,
"description": "The route handler is for GET /api/products/:id/social-proof (or equivalent parameterised path with the id segment)"
},
{
"name": "Parallel data fetching",
"max_score": 10,
"description": "The handler uses Promise.all (or equivalent concurrent await) to fetch recentOrders/purchases, activeVisitors, reviewSummary, and stockLevel in parallel rather than sequentially"
},
{
"name": "PII anonymization — fields",
"max_score": 10,
"description": "Each purchase entry in the response contains only firstName and a city/state or city/country location string — no full names, email addresses, or order IDs"
},
{
"name": "PII anonymization — no order IDs",
"max_score": 5,
"description": "The response/design-notes explicitly states that order IDs are NOT included in purchase notification data"
},
{
"name": "Redis sorted-set visitor tracking",
"max_score": 10,
"description": "The trackProductPageView function uses redis.zadd to record session presence and redis.zremrangebyscore to prune stale entries"
},
{
"name": "5-minute sliding window",
"max_score": 8,
"description": "The visitor tracking uses a 5-minute window (300,000 ms or 5 * 60 * 1000) for active visitor detection"
},
{
"name": "Redis key expiry 600s",
"max_score": 7,
"description": "redis.expire is called on the visitor key with 600 seconds"
},
{
"name": "Math.max floor for activeVisitors",
"max_score": 8,
"description": "The API response applies Math.max(count, 1) (or equivalent) so activeVisitors is never returned as 0"
},
{
"name": "Random boost for low counts",
"max_score": 8,
"description": "When the raw visitor count is below 3, a small random offset (e.g. Math.floor(Math.random() * 3)) is added to the count"
},
{
"name": "isLow threshold",
"max_score": 10,
"description": "The isLow flag in the stock level response is true when stockLevel > 0 AND stockLevel <= 5 (not 3, not 10 — specifically 5)"
},
{
"name": "24-hour purchase window",
"max_score": 7,
"description": "Recent orders are fetched using a 24-hour lookback window (last 24h from now)"
},
{
"name": "Async loading noted",
"max_score": 7,
"description": "design-notes.md mentions that the social proof data should be loaded asynchronously / after page load, not blocking the critical rendering path"
},
{
"name": "timeAgo included",
"max_score": 5,
"description": "Each purchase entry includes a timeAgo field (human-readable elapsed time) rather than a raw timestamp"
}
]
}
Build a Social Proof Data API for a Product Page
Problem/Feature Description
Your team is building the backend for a product page that needs to surface real-time trust signals to shoppers. The marketing team has identified that conversion rates on the product detail pages are low, and they believe showing live activity — such as who recently bought, how many people are viewing right now, and current stock levels — will reduce purchase hesitancy.
You have access to a PostgreSQL database (via a db ORM object) and a Redis instance (via a redis client). The database has orderLineItems (with order.shippingAddress containing firstName, city, stateCode, countryCode), productReviews, and productVariants tables. Redis holds session presence data. Your task is to implement the backend endpoint and the Redis-based visitor tracking utilities.
Output Specification
Produce a TypeScript file social-proof-api.ts that contains:
- An async Express-style route handler for a product social proof endpoint
- A function that records a product page view for a given session
- A function that retrieves the current active visitor count for a product
The code does not need to be runnable (no build step required), but it should be well-structured and production-quality TypeScript.
Also produce a short design-notes.md explaining:
- How the visitor count is computed and what time window is used
- How the API response avoids leaking customer personal data
- What the
isLowflag in the stock level response represents and when it is set - Any adjustments made to the raw visitor count before returning it in the response, and why
{
"name": "finsi/social-proof-widgets",
"version": "0.1.0",
"summary": "Display real-time social proof including recent purchases, review counts, visitor counts, and verified buyer badges to build trust and boost conversions",
"skills": {
"social-proof-widgets": {
"path": "SKILL.md"
}
}
}