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.
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.
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.
<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("有優惠碼嗎"); // falseProps
| Prop | Type | Default | Description |
|---|---|---|---|
messages | LiveMessage[] | — | Controlled message list (oldest to newest); when provided, internal state and simulate are disabled, and new messages should be written back through onMessagesChange |
defaultMessages | LiveMessage[] | [] | Initial messages in uncontrolled mode |
onMessagesChange | (messages: LiveMessage[]) => void | — | Callback whenever the message list changes (the "+1 喊單" button or a simulated message); in controlled mode, update messages from here |
simulate | boolean | false | In 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 |
product | LiveProduct | built-in fictional product (NT$680, stock 20) | Pinned product (name, price, starting stock) |
viewers | number | — | Controlled viewer count; when omitted the component simulates small fluctuations every few seconds |
defaultViewers | number | 1280 | Starting value for the simulated viewer count (only used when viewers is omitted) |
showViewers | boolean | true | Whether to show the viewer count in the top-right corner |
title | string | "週三晚上開賣" (Wednesday-night sale) | Stream title shown to the right of the LIVE badge |
selfName | string | "我" (me) | Account name used for the message sent by the "+1 喊單" button |
onPlusOne | (message: LiveMessage) => void | — | Fires once for every newly detected +1 message (including your own button presses) |
onHeart | (count: number) => void | — | Fires on every heart tap with the running total |
layout | "portrait" | "landscape" | "portrait" | Stage ratio: portrait 9:16 (mobile streams) or landscape 16:9 |
maxMessages | number | 5 | Maximum number of messages shown at once; older ones drift up and fade out |
children | React.ReactNode | — | Node placed on the base layer of the stage (e.g. a real video); it is stretched to fill and sits under every overlay |
className | string | — | Forwarded to the outermost stage container |
LiveMessage
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique identifier; the component uses it to tell "new" messages apart, and the same id never triggers +1 twice |
user | string | — | Account name (the avatar takes its first character and a colour hashed from the name) |
text | string | — | Message body; when it matches the +1 rule it is highlighted and counted as an order |
ts | number | — | Sent time (millisecond timestamp), optional and only for the caller's sorting or display |
LiveProduct
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Product name |
price | number | — | Price (New Taiwan dollars) |
stock | number | — | Starting 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 byid, so the sameidis never counted twice. +1s in the initial messages (defaultMessages, or the firstmessagesyou 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
messagesmakes it controlled — the component stops adding messages on its own (simulateis ignored) and the "+1 喊單" button writes back throughonMessagesChange([...messages, newMessage]), leaving it to you whether to send it to a backend. In uncontrolled mode the component keeps a few more messages thanmaxMessages, so raising the limit later still has older messages to fill in - Simulated stream: scheduled with a
setTimeoutchain rather than a fixedsetInterval, 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 fromdefaultViewerswith a small (about ±3%) drift every 3 seconds, rolling digit by digit; thousands grouping is implemented by hand instead oftoLocaleString, 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.randomis 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;
childrenis 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 anaria-labelcarrying the stream title; the heart button and "+1 喊單" are bothtype="button"withfocus-visiblerings, and the order button isdisabledonce sold out - The comment stream is
role="log"witharia-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 hiddenrole="status"region announce the running order count and remaining stock - The viewer count is not
aria-live(it changes every few seconds); insteadsr-onlytext adds the "人觀看中" (viewers watching) unit; the heart button'saria-labelcarries 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
Free Shipping Meter (React)
A multi-tier threshold meter (free shipping → gift → discount) for React: rolling 'NT$X more for free shipping' copy, a colour change across the whole bar and a one-shot micro-confetti burst when a tier is crossed, and reached tiers listed as chips — drop it into a cart header on its own.
Group Buy Progress (React)
A head-count group-buy card for React: stacked participant avatars, tiered group prices, a deadline countdown, a CTA that flips to "group formed" with a card pulse and confetti the moment the threshold is hit, and an automatic refund note if the deadline passes short of the minimum.