Page Sections

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

260
600
Active edited usageDefault props
<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.json

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

PropTypeDefaultDescription
testimonialsCaseNotePileItem[]—Case-note data; 3–5 items work best
eyebrowReactNode—Small label above the title
titleReactNode—Section title, also the section's accessible name
subtitleReactNode—Description under the title
valuenumber—Controlled mode: index of the selected note
defaultValuenumber0Uncontrolled mode: initially selected note index
onValueChange(index: number) => void—Called when the selected note changes
grainbooleanfalseOverlay a fine paper texture (feTurbulence noise)
showStampbooleantrueShow the result stamp
stiffnessnumber260Spring stiffness for pulling a note out; reads --wb-case-note-pile-stiffness when omitted
durationnumber600Highlighter sweep duration (ms); falls back to --wb-case-note-pile-duration when omitted
countLabelstring"case notes"Unit text next to the case count on the left
noteLabelstring"Case note"Header text in the top-left corner of each note
tabsLabelstring"Switch case"Accessible name of the thumbnail tab list
classNamestring—Merged into the root section element

The Chinese version of the component defaults these strings to "份案例筆記" (case notes), "案例筆記" (case note) and "切換案例" (switch case).

CaseNotePileItem

FieldTypeDefaultDescription
quotestring—Full quote text
namestring—Customer name; the avatar shows initials (last two characters for Chinese, first letters for Latin)
rolestring—Job title
companystring—Company name, also used on the thumbnail tab
ratingnumber—Rating (0–5), kept for the shared data shape and not rendered here
avatarHuenumbergolden angle by indexHue (0–360) for the avatar and photo placeholder
highlightstring—Fragment of the quote to highlight; must match the quote exactly
metric{ value: string; label: string }—Result stamp content
caseFieldsCaseNotePileField[]—Field table at the top ({ label, value })
photoSrcstring—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-size sweeps from 0% to 100% 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); the stiffness and duration props override them.
  • The accent only comes from --wb-case-note-pile-accent (default #0d9488 light, #2dd4bf dark); the highlighter, stamp and selected tab all follow it.

Accessibility

  • The root is a section labelled by its title through aria-labelledby
  • The thumbnail tabs are a role="tablist"; each tab is a role="tab" that points at its note via aria-controls; the arrow keys, Home and End move between tabs and switch notes
  • Each note is a role="tabpanel"; unselected notes are aria-hidden, and the selected note's content becomes readable once its tab is chosen
  • Quotes are rendered with figure, blockquote and figcaption, and the highlighted fragment uses mark
  • 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