WebberUI

Live Shopping Overlay (React)

A React live-selling UI overlay — on a fixed-ratio stage it layers a LIVE badge with a rolling viewer count, heart bursts, a scrolling comment stream, automatic "+1" order detection with floating counters, and a pinned product card with a stock countdown; drop in children to use a real video.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Taiwan's live-selling culture, on social and e-commerce streams alike, runs on "+1": the host presents a product and viewers type "+1" in the comments to place an order. This component packages the whole live-shop UI overlay into a fixed-ratio stage (portrait 9:16 or landscape 16:9). The base layer is a gradient with slowly drifting glows standing in for the video (pass children to use a real video), and layered on top are: a LIVE badge with a digit-by-digit rolling viewer count in the top-left, a heart button with floating particles on the right, a comment stream in the bottom-left (new messages slide in from the bottom, older ones drift up and fade, accounts get a colour-block initial as their avatar), automatic +1 detection (matching comments are highlighted, a "+1" floats up above the product card, and the order count and stock update live), and a pinned product card with a "+1 喊單" (order) button. The message list works controlled or uncontrolled, and simulate lets the component generate its own comments and viewer fluctuations; every account, comment and product is fictional demo data.

Loading preview…
npx shadcn@latest add "https://webberui.com/r/live-shopping-overlay.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

5
<LiveShoppingOverlay />

Installation

npx shadcn@latest add "https://webberui.com/r/live-shopping-overlay.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/live-shopping-overlay.

Usage

import {
  LiveShoppingOverlay,
  isPlusOne,
} from "@/components/ui/live-shopping-overlay";

// Demo mode: the comment stream and viewer count are simulated inside the component (fictional product and accounts)
<LiveShoppingOverlay
  simulate
  product={{ name: "陶瓷手沖濾杯組", price: 680, stock: 12 }}
  onPlusOne={(msg) => console.log("order", msg.user, msg.text)}
  onHeart={(count) => console.log("hearts", count)}
/>

// Wired to a backend: controlled messages, viewer count from your API, a real video under the overlay
<LiveShoppingOverlay
  layout="landscape"
  messages={messages}
  onMessagesChange={setMessages}
  viewers={viewers}
  product={product}
  onPlusOne={(msg) => createOrder(msg)}
>
  <video src={streamUrl} autoPlay muted playsInline />
</LiveShoppingOverlay>

// The order-detection helper can be used on its own (e.g. to filter comments on the server or at the form layer)
isPlusOne("+1 黑色"); // true
isPlusOne("我要 1 個"); // true
isPlusOne("有優惠碼嗎"); // false

Props

PropTypeDefaultDescription
messagesLiveMessage[]Controlled message list (oldest to newest); when provided, internal state and simulate are disabled, and new messages should be written back through onMessagesChange
defaultMessagesLiveMessage[][]Initial messages in uncontrolled mode
onMessagesChange(messages: LiveMessage[]) => voidCallback whenever the message list changes (the "+1 喊單" button or a simulated message); in controlled mode, update messages from here
simulatebooleanfalseIn uncontrolled mode, let the component simulate a comment stream (including random +1s) on a timer; for demos only — turn it off once wired to a backend
productLiveProductbuilt-in fictional product (NT$680, stock 20)Pinned product (name, price, starting stock)
viewersnumberControlled viewer count; when omitted the component simulates small fluctuations every few seconds
defaultViewersnumber1280Starting value for the simulated viewer count (only used when viewers is omitted)
showViewersbooleantrueWhether to show the viewer count in the top-right corner
titlestring"週三晚上開賣" (Wednesday-night sale)Stream title shown to the right of the LIVE badge
selfNamestring"我" (me)Account name used for the message sent by the "+1 喊單" button
onPlusOne(message: LiveMessage) => voidFires once for every newly detected +1 message (including your own button presses)
onHeart(count: number) => voidFires on every heart tap with the running total
layout"portrait" | "landscape""portrait"Stage ratio: portrait 9:16 (mobile streams) or landscape 16:9
maxMessagesnumber5Maximum number of messages shown at once; older ones drift up and fade out
childrenReact.ReactNodeNode placed on the base layer of the stage (e.g. a real video); it is stretched to fill and sits under every overlay
classNamestringForwarded to the outermost stage container

LiveMessage

FieldTypeDefaultDescription
idstringUnique identifier; the component uses it to tell "new" messages apart, and the same id never triggers +1 twice
userstringAccount name (the avatar takes its first character and a colour hashed from the name)
textstringMessage body; when it matches the +1 rule it is highlighted and counted as an order
tsnumberSent time (millisecond timestamp), optional and only for the caller's sorting or display

LiveProduct

FieldTypeDefaultDescription
namestringProduct name
pricenumberPrice (New Taiwan dollars)
stocknumberStarting stock; every +1 takes one unit, and at 0 the card shows sold out and disables the order button

isPlusOne(text) and the LiveShoppingLayout type are also named exports, so the same order-detection rule can be reused on the server or at the form layer.

How it works

  • +1 rule (isPlusOne): after trimming, the message either matches /(^|\s)\+?1(\s|$)/ (a standalone "1" or "+1", e.g. "1" or "我要 1 個" — I want 1) or contains "+1" / full-width "+1" anywhere (e.g. "+1黑色" — +1 black, or "幫我留一個+1" — save me one +1)
  • Order count and stock: every new +1 message takes one unit of stock and fires onPlusOne; the component tracks processed messages by id, so the same id is never counted twice. +1s in the initial messages (defaultMessages, or the first messages you pass) are folded into the starting order count but do not fire the callback or the floating animation, keeping SSR and client output identical
  • Controlled vs. uncontrolled: passing messages makes it controlled — the component stops adding messages on its own (simulate is ignored) and the "+1 喊單" button writes back through onMessagesChange([...messages, newMessage]), leaving it to you whether to send it to a backend. In uncontrolled mode the component keeps a few more messages than maxMessages, so raising the limit later still has older messages to fill in
  • Simulated stream: scheduled with a setTimeout chain rather than a fixed setInterval, with a deterministic jitter on every gap so it reads like real people rather than a metronome; roughly a 35% chance of an order per message, chat only once sold out; no messages are added while the tab is in the background so a burst of animations does not play all at once when you return; timers are cleared on unmount
  • Viewer count: controlled through viewers, or simulated from defaultViewers with a small (about ±3%) drift every 3 seconds, rolling digit by digit; thousands grouping is implemented by hand instead of toLocaleString, avoiding server/browser ICU differences that would cause hydration mismatches
  • The heart and "+1" particles are hand-rolled with motion; each particle's drift, sway, scale and tone come from a deterministic pseudo-random derived from its serial number, so Math.random is never called during render; particles are removed when their lifetime ends and their timers are cleared
  • The base "video" placeholder is a gradient with two slowly drifting glows, no external video or image; children is stretched to fill (object-cover) and sits under every overlay, and top/bottom darkening masks keep white text, comments and the product card readable over any footage
  • Stock countdown: the progress bar on the product card shortens with every +1, turns red at 3 units or fewer with a "最後 N 件" (last N left) label, and at 0 shows "已完售" (sold out) and disables the order button

Accessibility

  • The outermost element is role="region" with an aria-label carrying the stream title; the heart button and "+1 喊單" are both type="button" with focus-visible rings, and the order button is disabled once sold out
  • The comment stream is role="log" with aria-live="off" — a simulated message every second or two would drown a screen reader if each were announced; only when you press "+1 喊單" yourself does a hidden role="status" region announce the running order count and remaining stock
  • The viewer count is not aria-live (it changes every few seconds); instead sr-only text adds the "人觀看中" (viewers watching) unit; the heart button's aria-label carries the number of hearts sent
  • Every decorative element (glows, darkening masks, avatar blocks, particles, progress bar) is aria-hidden, while the comment text itself stays readable DOM content
  • When the user has "reduce motion" enabled at the system level, the drifting glows, the LIVE pulse dot, the heart and "+1" particles, the rolling digits and the slide-in of comments are all disabled, leaving only the number, highlight and text updates; the intervals of the simulated stream and viewer drift are doubled

On this page