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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | ColorBlockQuotesItem[] | — | Testimonials, in the same order as the name list on the right |
eyebrow | string | — | Small label above the title |
title | string | — | Section title; also the accessible name of the section |
subtitle | string | — | Supporting text below the title |
palette | string[] | — | Colour palette, cycled by index; falls back to the four defaults |
value | number | — | Controlled selected index; managed internally when omitted |
defaultValue | number | 0 | Initial selected index in uncontrolled mode |
onValueChange | (index: number) => void | — | Called when the selected index changes |
autoplay | boolean | false | Auto-advance; shows a pause button when on |
interval | number | 6000 | Auto-advance interval in milliseconds, minimum 1000 |
seed | number | 4 | Random seed for the generative pattern |
showPattern | boolean | true | Show the generative pattern inside the block |
pattern | "dots" | "arcs" | "dots" | Pattern style: dot grid or concentric arcs |
pauseLabel | string | "暫停自動播放" (Pause autoplay) | Pause button text (while playing) |
playLabel | string | "繼續自動播放" (Resume autoplay) | Pause button text (while paused) |
listLabel | string | "選擇評價" (Choose a testimonial) | Accessible name of the name list; also names the section when there is no title |
className | string | — | Class applied to the root section |
ColorBlockQuotesItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Quote text |
name | string | — | Reviewer name; the avatar shows initials (last two characters for CJK names, first letters for Latin names) |
role | string | — | Job title |
company | string | — | Company or organisation |
rating | number | — | Star rating (1–5); hidden when omitted |
avatarHue | number | — | Avatar gradient hue; assigned by index with the golden angle (137.508°) when omitted |
highlight | string | — | 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 |
color | string | — | Block colour for this testimonial; takes precedence over palette |
textColor | string | — | 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-1through-4(#0d9488,#d97706,#e11d48,#475569) so themes can override it; text colour is still computed from the default hex, so passtextColorif 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/paletteonly parse#rgb,#rrggbbandrgb()— other formats such asoklch()fall back to white text. - Transition length comes from
--wb-color-block-quotes-duration(default500ms), shared by the block recolour and the quote crossfade, on acubic-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
seedand 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#0d9488and#2dd4bfin 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"witharia-orientation="vertical"; each row is arole="tab"linked to the block througharia-selectedandaria-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 viaaria-live="polite" - Quotes are marked up with
figure,blockquoteandfigcaption; 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
Instant Photo Stack
A pile of askew instant photos with the quote on the top print; click or flick it away to send it to the bottom, bringing the next one up as the stack re-fans.
Filterable Review Grid
A dense React grid of small testimonial cards that reflow with a layout animation when filtered by industry, lifting on hover with a border that lights up along the pointer, and expanding in place on click.