Review Ticker
A React single-line, endlessly scrolling ticker of short testimonials separated by stars and fading at both edges, with an optional reverse second line, pausing on hover or focus and a visible pause button.
A social-proof band for under the hero. Each testimonial is just a small avatar, one short line and "— name, company", separated by accent-coloured stars and fading out at both edges. The content is duplicated and scrolled endlessly with a linear CSS animation; speed is given in px/s and converted into a period once the content width is measured. With rows={2} the second row scrolls the other way, offset by half. It pauses automatically on mouse hover, keyboard focus, when the tab is hidden or when the band leaves the viewport, and a visible pause button sits on the right.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add review-ticker.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ReviewTicker/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/review-ticker.en.jsonOr, once registries are configured in components.json, install it as @webberui/review-ticker.
Usage
import { ReviewTicker } from "@/components/ui/review-ticker";
<ReviewTicker
rows={2}
speed={50}
testimonials={[
{ quote: "門市排班從半天縮到十分鐘,店長終於能準時下班。", name: "林予安", company: "青嶼咖啡" },
{ quote: "出貨異常的通知時間縮短了一半,客服再也不用一通通電話追司機。", name: "陳柏宏", company: "北辰物流", short: "出貨異常通知時間縮短一半" },
{ quote: "結帳流程更短,這一季回購率是開站以來最高。", name: "黃冠廷", company: "好日選物" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | ReviewTickerItem[] | — | Array of testimonials |
eyebrow | string | — | Small label above the title; shown only when provided |
title | string | — | Section title; hidden by default, placed above the ticker when provided |
subtitle | string | — | Supporting text below the title |
rows | 1 | 2 | 1 | Number of rows; with 2 the second row scrolls the other way, offset by half |
speed | number | 50 | Scroll speed (px/s), converted into a period after measuring the content width |
direction | "left" | "right" | "left" | Scroll direction of the first row |
bordered | boolean | true | Whether to show thin top and bottom borders |
duration | number | — | Fixed period (seconds); overrides the value derived from speed |
maxQuoteLength | number | 40 | Quotes longer than this many characters are truncated with … (not applied when short is set) |
paused | boolean | — | Controlled: whether the user has paused the ticker |
defaultPaused | boolean | false | Uncontrolled: whether the ticker starts paused |
onPausedChange | (paused: boolean) => void | — | Called when the user presses the pause button |
regionLabel | string | "Customer testimonials" | Accessible label for the section when no title is provided |
pauseLabel | string | "Pause scrolling" | Accessible label of the pause button |
className | string | — | Merged into the root section |
ReviewTickerItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote text; when truncated, hovering shows the full text |
name | string | — | Reviewer name |
company | string | — | Company name, shown after the name |
role | string | — | Job title; shown instead when company is not provided |
short | string | — | Short version shown on the ticker; quote is truncated automatically when omitted |
avatarHue | number | — | Avatar gradient hue (0–360); derived from the index by the golden angle when omitted |
avatarSrc | string | — | Avatar image URL; shows an initials gradient circle when omitted |
highlight | string | — | Fragment of the displayed text to mark in the accent colour |
rating | number | — | Not displayed here; kept so data can be shared with other testimonial components |
metric | { value: string; label: string } | — | Not displayed here; kept so data can be shared with other testimonial components |
How it works
- Timing variables: the period reads
--wb-review-ticker-duration(derived fromspeedby default, 45s before measurement), the entrance fade reads--wb-review-ticker-enter-duration(600ms), and the accent reads--wb-review-ticker-accent(default#0d9488,#2dd4bfin dark mode). Setting--wb-review-ticker-durationglobally overridesspeed. - Seamless loop: the content is duplicated and
translateXruns from 0 to -50%. With only a few testimonials, when one set is narrower than the viewport, the set is repeated until it fills the width before being duplicated, so no gap shows. - Truncation counts characters for both Chinese and Latin text, so no character is ever cut in half. Hovering a truncated quote shows the full text above the band, and screen readers read the full quote directly.
- No photos needed: avatars use initials (last two characters for Chinese names, first letters for Latin names) on a two-tone gradient whose hue follows the golden angle by index, generated deterministically; pass
avatarSrcfor a real image. - The background is a very faint wave line SVG built from a few summed sines, producing identical output on server and client.
Accessibility
- The root is a
section; with atitleit usesaria-labelledby, otherwisearia-label="Customer testimonials" - The duplicated loop content and the second row (a repeat of the same testimonials) are
aria-hidden, so screen readers read each testimonial once - Each testimonial is a
figure/blockquote/figcaption; when truncated, the visible text is hidden from assistive tech and the full quote is read instead - A visible pause button sits on the right of the band (
aria-pressedconveys the state), meeting WCAG 2.2.2; the ticker also pauses on hover, keyboard focus, a hidden tab or when scrolled out of view - When the user has enabled reduce motion, nothing scrolls: the ticker becomes a single static row that can be focused and scrolled horizontally with the keyboard, the entrance fade shortens to 150ms, and the pause button is hidden
Avatar Tab Quotes
A testimonial section React component: a vertical column of avatars and names acts as tabs, the selection marker slides along the column, and the quote on the right switches with a blurred crossfade while the avatar scales up.
Typed Quote Row
A React row of testimonial cards that take turns typing out their quotes character by character with a blinking caret; after a pause the next card begins, while the others show their full text dimmed.