Page Sections

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.

A testimonial block for pages with lots of white space: no card frame and no avatar — just one large centred quote, a single author line and a row of fine dots. On each switch the old sentence drifts up, fades and blurs slightly while the new one rises in and comes into focus, the two overlapping as they pass; the author line follows 120ms later. Behind it, a few hairline curves generated from seed flow slowly on a 20-second cycle. The quote area is fixed to the height of the longest quote, so the layout never jumps between switches.

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 one-line-quote-carousel.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.

5000
600
9
Active edited usageDefault props
<OneLineQuoteCarousel/>

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/one-line-quote-carousel.en.json

Or, once registries are configured in components.json, install it as @webberui/one-line-quote-carousel.

Usage

import { OneLineQuoteCarousel } from "@/components/ui/one-line-quote-carousel";

<OneLineQuoteCarousel
  testimonials={[
    {
      quote: "Since the redesign, our weekend waitlist is half as long.",
      name: "Maya Lin",
      role: "Head of Operations",
      company: "Islet Coffee",
      highlight: "half as long",
    },
    {
      quote: "New drivers run a full route on their own from day one.",
      name: "Owen Chen",
      company: "Northstar Freight",
    },
  ]}
  interval={5000}
  seed={9}
  onIndexChange={(i) => console.log(`Switched to ${i + 1}`)}
/>

Props

PropTypeDefaultDescription
testimonialsOneLineQuoteCarouselItem[]—List of testimonials
eyebrowstring—Small label above the title
titlestring—Section title; not shown by default, and when provided it becomes the section's aria-labelledby
subtitlestring—Supporting text below the title
autoplaybooleantrueWhether to autoplay; always off when the user prefers reduced motion
intervalnumber5000Autoplay interval (ms); reads --wb-one-line-quote-carousel-interval when omitted
durationnumber600Duration (ms) of the quote cross-fade; reads --wb-one-line-quote-carousel-duration when omitted
indexnumber—Controlled current index; managed internally when omitted
defaultIndexnumber0Initial index in uncontrolled mode
onIndexChange(index: number) => void—Called when the index changes
showBackdropbooleantrueWhether to show the flowing hairline backdrop
seednumber9Random seed for the backdrop; the same seed always draws the same 3–5 curves
classNamestring—Forwarded to the root section

OneLineQuoteCarouselItem

FieldTypeDefaultDescription
quotestring—Quote text; one sentence works best
namestring—Name
rolestring—Job title, appended after the name with " · "
companystring—Company
highlightstring—Fragment of the quote to mark in the accent colour
ratingnumber—Rating (0–5); not shown here, kept so data can be shared with other testimonial components
avatarHuenumber—Hue; this component shows no avatar, kept so data can be shared
metric{ value: string; label: string }—Key figure; not shown here, kept so data can be shared

How it works

  • Timing variables: --wb-one-line-quote-carousel-interval (default 5000ms) and --wb-one-line-quote-carousel-duration (default 600ms). Passing interval / duration writes them into these variables, and the component reads them back from the computed style after mounting, so overriding the variables from an outer scope works too. The easing is fixed at cubic-bezier(0.22,1,0.36,1).
  • The accent colour only comes from --wb-one-line-quote-carousel-accent (light default #0d9488, dark default #2dd4bf), used for the highlight, the eyebrow and the active dot.
  • The quote area stacks every quote invisibly in one grid cell as a "ruler", so its height always matches the longest quote; the current quote is absolutely positioned on top, and both old and new sentences exist during the cross-fade.
  • The backdrop curves come from a deterministic PRNG (mulberry32) seeded by seed, so server and browser draw identical paths; strokes stay 1px (vector-effect: non-scaling-stroke) and both ends fade out through a mask.
  • Autoplay pauses on hover, when keyboard focus is inside the carousel, when the pause button is pressed and when the tab goes to the background; every switch or resume restarts a full interval.

Accessibility

  • The root element is a section, labelled by the title via aria-labelledby when there is one and by aria-label otherwise; the carousel area is role="region" with aria-roledescription="carousel", is focusable, and the ← / → keys switch quotes while it has focus
  • Quotes are marked up with figure, blockquote and figcaption; the invisible ruler is aria-hidden so nothing is read twice
  • aria-live="off" while autoplay runs so screen readers are not interrupted on a timer; it switches to polite when paused or switched manually
  • Each dot is a type="button" with an aria-label such as "Testimonial 2: name", and the active one carries aria-current
  • With autoplay on there is always a visible pause button at the end of the dot row (WCAG 2.2.2), with aria-pressed; hovering or focusing also pauses
  • When the system "Reduce motion" setting is on: autoplay is off, the hairlines stop flowing, quotes no longer move or blur and fade in over 150ms instead, and the dots still switch quotes