Case Note Pile
Each testimonial becomes a case note — structured rollout details, a paper-clipped photo and a highlighter over the key line — and the selected note is pulled to the front of the pile.
Customer testimonials laid out as a loose pile of case notes. Each sheet opens with a field table of rollout details (case number, industry, rollout period, team size), holds a small photo under a paper clip in the top-right corner, marks the key line of the quote with an accent-colored highlighter, and carries a result stamp at the bottom. Click any sheet in the pile, or switch with the thumbnail tabs below, and the selected note springs upright, moves to the center and is pulled to the top — then the highlighter sweeps left to right and the stamp comes down. The selected index works in both controlled and uncontrolled modes.
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 case-note-pile.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CaseNotePile/>
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/case-note-pile.en.jsonOr, once registries are configured in components.json, install it as @webberui/case-note-pile.
Usage
import { CaseNotePile } from "@/components/ui/case-note-pile";
<CaseNotePile
eyebrow="Customer stories"
title="Every rollout left a note behind"
subtitle="See how teams of different sizes got their workflows running smoothly."
testimonials={[
{
quote:
"Drivers see reassignments on the road, and delivery is steady enough that customers order more on their own.",
highlight: "customers order more on their own",
name: "Ethan Chen",
role: "Dispatch Manager",
company: "Polaris Freight",
metric: { value: "-32%", label: "Late deliveries" },
caseFields: [
{ label: "Case no.", value: "No.031" },
{ label: "Industry", value: "Logistics" },
{ label: "Rollout", value: "10 weeks" },
{ label: "Team size", value: "64" },
],
},
// ...3–5 items in total work best
]}
onValueChange={(index) => console.log(`Selected note ${index + 1}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
testimonials | CaseNotePileItem[] | — | Case-note data; 3–5 items work best |
eyebrow | ReactNode | — | Small label above the title |
title | ReactNode | — | Section title, also the section's accessible name |
subtitle | ReactNode | — | Description under the title |
value | number | — | Controlled mode: index of the selected note |
defaultValue | number | 0 | Uncontrolled mode: initially selected note index |
onValueChange | (index: number) => void | — | Called when the selected note changes |
grain | boolean | false | Overlay a fine paper texture (feTurbulence noise) |
showStamp | boolean | true | Show the result stamp |
stiffness | number | 260 | Spring stiffness for pulling a note out; reads --wb-case-note-pile-stiffness when omitted |
duration | number | 600 | Highlighter sweep duration (ms); falls back to --wb-case-note-pile-duration when omitted |
countLabel | string | "case notes" | Unit text next to the case count on the left |
noteLabel | string | "Case note" | Header text in the top-left corner of each note |
tabsLabel | string | "Switch case" | Accessible name of the thumbnail tab list |
className | string | — | Merged into the root section element |
The Chinese version of the component defaults these strings to "份案例筆記" (case notes), "案例筆記" (case note) and "切換案例" (switch case).
CaseNotePileItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Full quote text |
name | string | — | Customer name; the avatar shows initials (last two characters for Chinese, first letters for Latin) |
role | string | — | Job title |
company | string | — | Company name, also used on the thumbnail tab |
rating | number | — | Rating (0–5), kept for the shared data shape and not rendered here |
avatarHue | number | golden angle by index | Hue (0–360) for the avatar and photo placeholder |
highlight | string | — | Fragment of the quote to highlight; must match the quote exactly |
metric | { value: string; label: string } | — | Result stamp content |
caseFields | CaseNotePileField[] | — | Field table at the top ({ label, value }) |
photoSrc | string | — | Photo held by the paper clip; falls back to a gradient + portrait silhouette placeholder |
How it works
- Scatter: each note's offset and rotation (-4 to 4deg) come from a fixed position table with no randomness, so server and browser renders match.
- Stack order: the selected note is always on top; the note it replaced stays second and springs back to its own scatter position, as if it had just been put back on the pile.
- Highlighter:
background-sizesweeps from0%to100%left to right after a 200ms delay; wrapped lines each get their own stroke. - Timing variables:
--wb-case-note-pile-stiffness(spring stiffness, default 260, damping fixed at 26) and--wb-case-note-pile-duration(highlighter duration, default 600ms); thestiffnessanddurationprops override them. - The accent only comes from
--wb-case-note-pile-accent(default#0d9488light,#2dd4bfdark); the highlighter, stamp and selected tab all follow it.
Accessibility
- The root is a
sectionlabelled by its title througharia-labelledby - The thumbnail tabs are a
role="tablist"; each tab is arole="tab"that points at its note viaaria-controls; the arrow keys, Home and End move between tabs and switch notes - Each note is a
role="tabpanel"; unselected notes arearia-hidden, and the selected note's content becomes readable once its tab is chosen - Quotes are rendered with
figure,blockquoteandfigcaption, and the highlighted fragment usesmark - The initials avatar, photo placeholder and paper clip are decorative and hidden from assistive technology
- When the system asks to reduce motion, notes no longer spring into place and the selected one fades in over 150ms instead; the highlighter appears immediately, the stamp only fades, and every note is still reachable through the tabs
Masonry Review Wall
A masonry wall of testimonial cards of varying heights, each column moving at its own parallax speed as you scroll over a slowly morphing generative backdrop, with a show-more expansion.
Portrait Overlay Quote
A React testimonial section with the quote overlaid on a full-bleed portrait; switching crossfades the image with a slow push-in while the text rises line by line from a mask, with optional autoplay and drag to switch.