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.
A row of three customer testimonial cards where one card at a time is active: its quote types out character by character (one CJK character at a time, emoji never split), pausing briefly after punctuation, with a blinking accent caret at the end. After a hold, the active card dims and the next one starts typing from the top, wrapping back to the first after the last. The active card gets an accent top edge and a shadow, and the edge slides to the new card on each switch; the other cards show their full text dimmed. Clicking any card makes it active immediately and retypes it. Faint ruled-paper lines sit behind the row. The active index supports both controlled and uncontrolled use.
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 typed-quote-row.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<TypedQuoteRow/>
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/typed-quote-row.en.jsonOr, once registries are configured in components.json, install it as @webberui/typed-quote-row.
Usage
import { TypedQuoteRow } from "@/components/ui/typed-quote-row";
<TypedQuoteRow
eyebrow="客戶怎麼說"
title="一句一句,說給你聽"
speed={45}
hold={2000}
testimonials={[
{
quote: "導入後櫃台結帳快了一倍,尖峰時段不再排到門外。",
name: "林語晴",
role: "店長",
company: "青嶼咖啡",
highlight: "結帳快了一倍",
},
{
quote: "司機端的路線指引很直覺,查件電話少了一大半!",
name: "王子豪",
role: "物流主任",
company: "北辰物流",
},
{
quote: "學員的進度一目了然,結業率比去年高出不少。",
name: "張雅雯",
role: "教務長",
company: "拾光學院",
},
]}
onActiveIndexChange={(index) => console.log(index)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | — | Small label above the title |
title | string | — | Section title |
subtitle | string | — | Supporting text below the title |
testimonials | TypedQuoteRowItem[] | — | Testimonials (3 recommended, laid out in one row) |
speed | number | — | Delay between typed characters (ms); falls back to --wb-typed-quote-row-speed (default 45ms) |
hold | number | 2000 | How long a finished quote stays before the next card starts (ms) |
loop | boolean | true | Whether to return to the first card after the last one |
autoplay | boolean | true | Whether to advance automatically; off by default when the OS asks to reduce motion |
activeIndex | number | — | Controlled active card index; managed internally when omitted |
defaultActiveIndex | number | 0 | Initial active card index in uncontrolled mode |
onActiveIndexChange | (index: number) => void | — | Called when the active card changes, with the new index |
pauseLabel | string | "暫停輪播" (Pause autoplay) | Pause button label |
playLabel | string | "繼續輪播" (Resume autoplay) | Resume button label |
className | string | — | Classes merged into the root section |
TypedQuoteRowItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Full quote text |
name | string | — | Person's name |
role | string | — | Job title |
company | string | — | Company name |
rating | number | — | Star rating (0–5); hidden when omitted |
avatarHue | number | — | Avatar gradient hue (0–360); defaults to a golden-angle hue based on the index |
avatarSrc | string | — | Avatar image URL; replaces the initials gradient avatar when provided |
highlight | string | — | Fragment of the quote to mark with the accent color (colored as it types, too) |
metric | { value: string; label: string } | — | Optional result metric |
How it works
- Quotes are split into graphemes with
Intl.Segmenter, so CJK text types one character at a time and emoji or combining marks are never broken apart; punctuation such as,。!?、adds an extra 160ms pause. - An invisible copy of the full quote reserves the final height and the typing layer sits on top, so the card height never changes from the first character to the last and the layout does not jump.
- Typing and rotation run only while the section is in view and pause when it scrolls away or the tab is hidden; while the pointer or keyboard focus is on the row, a finished card stays put instead of handing off.
- Typing progress is keyed on "active index + retype count", so changing
activeIndexfrom outside in controlled mode also restarts the typing. - No photos needed for avatars: CJK names use the last two characters, Latin names use initials, paired with a two-tone gradient whose hue is assigned by index using the golden angle (137.508°) — deterministic and identical on server and client.
- Rhythm and color can be overridden with CSS variables:
--wb-typed-quote-row-speed(typing delay, default 45ms) and--wb-typed-quote-row-accent(accent color, default#0d9488,#2dd4bfin dark mode).
Accessibility
- The root is a
sectionlabelled by its title viaaria-labelledby; each testimonial is afigure/blockquote/figcaption - The typing layer and the height-reserving copy are hidden from assistive tech, and each card keeps a
sr-onlycopy of the full quote; active-card changes are not announced live, to avoid constantly interrupting screen readers - Each card is covered by a
type="button"switch witharia-pressedmarking the active card, and shows a focus-visible ring when tabbed to - While autoplaying, a visible pause/resume button is provided (WCAG 2.2.2); pausing completes the active quote immediately
- When the OS asks to reduce motion, nothing is typed and the caret does not blink: the active card shows its full text, switches become a 150ms fade, autoplay is off by default, and cards can still be clicked to switch
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.
One-Line Quote Carousel
A minimal React carousel showing a single large-type sentence; quotes cross-fade vertically with a slight blur, leaving only the author and fine dot indicators below over slowly flowing hairlines.