Page Sections

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.en

New project or installation trouble? Start here →

Playground

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

45
2000
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
eyebrowstring—Small label above the title
titlestring—Section title
subtitlestring—Supporting text below the title
testimonialsTypedQuoteRowItem[]—Testimonials (3 recommended, laid out in one row)
speednumber—Delay between typed characters (ms); falls back to --wb-typed-quote-row-speed (default 45ms)
holdnumber2000How long a finished quote stays before the next card starts (ms)
loopbooleantrueWhether to return to the first card after the last one
autoplaybooleantrueWhether to advance automatically; off by default when the OS asks to reduce motion
activeIndexnumber—Controlled active card index; managed internally when omitted
defaultActiveIndexnumber0Initial active card index in uncontrolled mode
onActiveIndexChange(index: number) => void—Called when the active card changes, with the new index
pauseLabelstring"暫停輪播" (Pause autoplay)Pause button label
playLabelstring"繼續輪播" (Resume autoplay)Resume button label
classNamestring—Classes merged into the root section

TypedQuoteRowItem

FieldTypeDefaultDescription
quotestring—Full quote text
namestring—Person's name
rolestring—Job title
companystring—Company name
ratingnumber—Star rating (0–5); hidden when omitted
avatarHuenumber—Avatar gradient hue (0–360); defaults to a golden-angle hue based on the index
avatarSrcstring—Avatar image URL; replaces the initials gradient avatar when provided
highlightstring—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 activeIndex from 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, #2dd4bf in dark mode).

Accessibility

  • The root is a section labelled by its title via aria-labelledby; each testimonial is a figure/blockquote/figcaption
  • The typing layer and the height-reserving copy are hidden from assistive tech, and each card keeps a sr-only copy 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 with aria-pressed marking 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