I came in because I could not switch off. Six months later I still work hard, but the work stays at the desk. Nobody fixed me. I just slowly stopped needing to be fixed.
After the divorce I did not know which parts of me were mine. The sessions gave me somewhere to lay everything out and pick up only what I wanted to carry.
Long-form testimonials told as stories: a serif heading settling in word by word beside the intro and a pill action, then alternating rows that pair a drifting portrait photograph with a story title, a serif quote, the person, and a link to the full account.
Testimonial48: Client Stories
Client stories as alternating rows: each pairs a portrait that drifts inside its frame with a small-caps story title, a serif quote, an attribution and a link to the full account.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
This installs the block to components/beste/block/testimonial48.tsx plus the button22 sliding-marker pill and the text1 word stagger it uses.
Quick start
The installed file exports testimonial48Demo alongside the block: the exact props behind the preview above. Spread it to get a working stories section in one line.
tsx
import { Testimonial48, testimonial48Demo } from "@/components/beste/block/testimonial48";export default function Page() { return <Testimonial48 {...testimonial48Demo} />;}
Then replace the demo with your own props. Written out, a trimmed setup looks like this:
tsx
import { Testimonial48 } from "@/components/beste/block/testimonial48";export default function Page() { return ( <Testimonial48 eyebrow="Stories" heading="Ordinary people. Quiet, stubborn change." description="Nobody here arrived with a plan." button={{ label: "All stories", href: "/stories", tone: "outline" }} stories={[ { title: "Learning to stop at the end of the day", quote: "I came in because I could not switch off. Six months later the work stays at the desk.", name: "Agnes Obel", context: "Eighteen months with the practice", href: "/stories/agnes", image: { src: "/people/agnes.jpg", alt: "Portrait of a client" }, }, ]} labels={{ readStory: "Read the story" }} /> );}
The alternation uses explicit md:col-start values on index % 2 while both cells stay in md:row-start-1, so the sides flip without the DOM order changing; the portrait always comes first in the markup, which keeps the reading order sane on every row.
Below md those column starts drop away and every row stacks portrait-then-quote, so the alternation never produces an inconsistent order on a phone.
Quotes are real blockquote elements inside article wrappers, so the words are marked up as quoted material rather than as styled paragraphs.
Each title is a separate small-caps line above the quote, so a long story can be summarised without turning the quote itself into a heading.
Every portrait owns its own scroll listener, so the rows do not drift in lockstep.
Rows are spaced with a gap-20 growing to gap-32 rather than rules, so the section reads as a sequence of scenes.
The story link's arrow lifts only under motion-safe:, so the affordance is dropped for reduced motion while the link keeps working.