Page Sections

Color Block Quotes

A testimonial section React component: large colour blocks carry the quotes; choosing a name from the list recolours and gently squeezes the block, slides the selection bar to the new row and crossfades the quote, over a seeded dot or arc pattern.

A testimonial section where a single block of colour holds up one customer quote. On the left is a large block with an oversized quote, the author and an optional result figure; on the right is the author list — each row has an initials avatar, name and company — acting as tabs. On change the block switches to that testimonial's colour with a scaleX spring squeeze, a selection bar in the same colour slides from the previous row to the new one behind the list, and the old quote fades up and out while the new one rises in. The generative pattern inside the block (a dot grid or concentric arcs) is derived deterministically from seed and re-emerges from the centre outward on every colour change. Text colour is picked automatically — white or neutral-900 — from the block luminance, keeping contrast at 4.5:1 or above. The selected index supports controlled and uncontrolled modes, with optional autoplay.

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 color-block-quotes.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.

6000
500
Active edited usageDefault props
<ColorBlockQuotes/>

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/color-block-quotes.en.json

Or, once registries are configured in components.json, install it as @webberui/color-block-quotes.

Usage

import { ColorBlockQuotes } from "@/components/ui/color-block-quotes";

<ColorBlockQuotes
  eyebrow="Customer stories"
  title="Small teams, sharp pages"
  testimonials={[
    {
      quote: "Since the new booking page went live, weekend guests finally stopped calling to ask about the wait.",
      name: "Lena Hart",
      role: "Store Manager",
      company: "Islet Coffee",
      highlight: "stopped calling",
      metric: { value: "−62%", label: "Phone enquiries" },
    },
    {
      quote: "Drivers understand the day's route the moment they open it. Handover time is down by half.",
      name: "Marcus Bell",
      role: "Head of Operations",
      company: "Northstar Logistics",
    },
  ]}
  onValueChange={(index) => console.log(`switched to #${index + 1}`)}
/>

Props

PropTypeDefaultDescription
testimonialsColorBlockQuotesItem[]—Testimonials, in the same order as the name list on the right
eyebrowstring—Small label above the title
titlestring—Section title; also the accessible name of the section
subtitlestring—Supporting text below the title
palettestring[]—Colour palette, cycled by index; falls back to the four defaults
valuenumber—Controlled selected index; managed internally when omitted
defaultValuenumber0Initial selected index in uncontrolled mode
onValueChange(index: number) => void—Called when the selected index changes
autoplaybooleanfalseAuto-advance; shows a pause button when on
intervalnumber6000Auto-advance interval in milliseconds, minimum 1000
seednumber4Random seed for the generative pattern
showPatternbooleantrueShow the generative pattern inside the block
pattern"dots" | "arcs""dots"Pattern style: dot grid or concentric arcs
pauseLabelstring"暫停自動播放" (Pause autoplay)Pause button text (while playing)
playLabelstring"繼續自動播放" (Resume autoplay)Pause button text (while paused)
listLabelstring"選擇評價" (Choose a testimonial)Accessible name of the name list; also names the section when there is no title
classNamestring—Class applied to the root section

ColorBlockQuotesItem

FieldTypeDefaultDescription
quotestring—Quote text
namestring—Reviewer name; the avatar shows initials (last two characters for CJK names, first letters for Latin names)
rolestring—Job title
companystring—Company or organisation
ratingnumber—Star rating (1–5); hidden when omitted
avatarHuenumber—Avatar gradient hue; assigned by index with the golden angle (137.508°) when omitted
highlightstring—Fragment of the quote to underline; must match the quote text exactly
metric{ value: string; label: string }—Result figure in the bottom-right of the block
colorstring—Block colour for this testimonial; takes precedence over palette
textColorstring—Text colour on the block; picked from luminance when omitted

How it works

  • The default palette maps to the CSS variables --wb-color-block-quotes-1 through -4 (#0d9488, #d97706, #e11d48, #475569) so themes can override it; text colour is still computed from the default hex, so pass textColor if you override with a colour of very different luminance.
  • Automatic text colour compares the contrast of white and neutral-900 directly and takes the higher one; all four defaults clear 4.5:1. color / palette only parse #rgb, #rrggbb and rgb() — other formats such as oklch() fall back to white text.
  • Transition length comes from --wb-color-block-quotes-duration (default 500ms), shared by the block recolour and the quote crossfade, on a cubic-bezier(0.22, 1, 0.36, 1) curve; the selection bar slides on a spring (stiffness 400, damping 34).
  • The pattern is generated with mulberry32 from seed and the selected index, so server and browser output match; dots appear with a 3ms stagger ordered by distance from the centre, with the whole layer at 10% opacity in the text colour.
  • The accent colour (eyebrow) uses --wb-color-block-quotes-accent, defaulting to #0d9488 and #2dd4bf in dark mode.
  • Autoplay pauses on hover, while keyboard focus is inside the section and when the tab is hidden; once the pause button has been pressed, the user's choice wins.

Accessibility

  • The name list is role="tablist" with aria-orientation="vertical"; each row is a role="tab" linked to the block through aria-selected and aria-controls. It uses a roving tabindex: ↑ / ↓ switch, Home / End jump to the first and last testimonial
  • The block is a role="tabpanel" labelled by the selected tab and focusable with Tab; when not autoplaying, new quotes are announced via aria-live="polite"
  • Quotes are marked up with figure, blockquote and figcaption; avatars and the pattern are decorative and hidden from assistive technology
  • With autoplay on, a visible, text-labelled pause button is always shown (WCAG 2.2.2)
  • Text colour is chosen from the block luminance; every default palette colour reaches at least 4.5:1 contrast
  • When the user has enabled reduce motion, autoplay is off by default (the play button can still start it), and the block squeeze, bar slide and quote movement become a 150ms fade with no per-dot stagger