Prism Testimonial

ProShowcase & Mediapowered by @vectorvesper/motion

Testimonial cards arranged as a hinged prism — the active quote faces you square on while its neighbours fold away at an angle. Drag, arrow-key or auto-rotate through them, with a spring carrying the turn and the whole assembly tilting slightly toward the pointer.

Prism Testimonial

Included with membership

Members install Prism Testimonial with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.

When to use it

A wall of social proof that should not read as a list — a landing page, a pricing page, an about page. The prism shows one quote properly and hints at the rest, which suits a handful of strong testimonials better than a dozen weak ones. It is a section: give it width, and content of its own above and below.

  • Give every item a stable id — it keys the faces, and a changing id snaps the prism mid-turn.
  • Use three to seven testimonials. Below three the prism barely reads as one; well above seven the far faces are never seen.
  • Keep quotes to a sentence or two. The face is a fixed box, and long copy either shrinks or clips.
  • Pass activeIndex with onIndexChange when something else on the page needs to drive the prism; leave both off and it manages itself.

Avoid:

  • Do not leave autoRotateInterval running on a page where people are reading. It turns under them mid-sentence — pauseOnHover helps but only once the pointer is already there.
  • Do not disable enableDrag, enableKeyboard and showStepControls together; that leaves auto-rotation as the only way through, and no way back.
  • Do not raise wingRotationDeg far past the default expecting more drama — past roughly 35 degrees the side faces turn edge-on and disappear.
  • Do not rely on the scaling to rescue a genuinely cramped box. It measures its container and shrinks the whole assembly to fit, but it stops at about two-thirds size — below that the side faces start clipping again.

Props

Testimonials arranged around a hinged prism. The one you are reading faces you square on; the others fold away to either side and sit further back. Drag, use the arrow keys, or let it turn on its own — a spring carries the rotation, and the whole assembly leans a little toward the pointer.

<PrismTestimonial />

The prism, its controls and the ticker beneath it. Paints its own background, so treat it as a section.

PropTypeDefaultDescription
items*TestimonialItem[]—The testimonials, in order. Required — nothing renders without it.
cardWidthnumber360Width of one face, in pixels.
cardHeightnumber310Height of one face. Quotes are laid out inside this box, so it caps how much copy fits. Scaled down with everything else in a small container.
cardSpacingnumber460Distance between neighbouring faces along the prism. Wider separates them; narrower overlaps them into a tighter fan.
depthRecessionnumber-130How far the side faces sit back from the active one. Negative pushes them away from the viewer — that is what makes it read as depth rather than a row.
wingRotationDegnumber22How far the side faces are turned, in degrees. This is the prop that sets the character; past about 35 they go edge-on.
perspectivestring"1600px"Depth of the 3D scene as a CSS length. Smaller values exaggerate the perspective, larger ones flatten it.
parallaxStrength{ pitch?: number; yaw?: number }{ pitch: 3.0, yaw: 4.5 }Degrees the whole prism leans toward the pointer. Small by design — it is meant to feel alive, not to be noticed.
springStiffnessnumber32.0How hard the prism is pulled toward the next face.
springDampingnumber11.5How quickly the turn settles. Lower lets it overshoot and rock back.
maxVelocitynumber8.5Ceiling on rotation speed, so a hard flick cannot spin the prism out of control.
dragSensitivitynumber340Pixels of drag per face. Higher means you have to pull further to advance.
activeIndexnumber—Drives the prism from outside. Supply it and the component becomes controlled — pair it with onIndexChange or it will not move.
defaultIndexnumber0Which face starts forward when the prism manages its own state.
onIndexChange(index: number, item: TestimonialItem) => void—Fires whenever a new face comes forward, however it was turned.
autoRotateIntervalnumber5000Milliseconds between automatic turns. Set 0 to leave it still until someone interacts.
enableDragbooleantruePointer dragging turns the prism.
enableKeyboardbooleantrueArrow keys turn the prism when it has focus. Leave this on — it is the accessible route through.
pauseOnHoverbooleantrueAuto-rotation stops while the pointer is over the prism.
showStepControlsbooleantrueRenders previous and next buttons beneath the prism.
showRatingbooleantrueShows the star rating on each face.
defaultRatingnumber5Rating used for any item that does not carry its own.
showMetricsbooleantrueShows the metric value and label at the top of a face, when the item supplies them.
showAvatarsbooleantrueShows the author photo in the footer of a face.
showTickerbooleantrueRenders a scrolling band of company names beneath the prism.
tickerSpeednumber35How fast that band travels. It holds still under reduced motion regardless.
renderTickerItem(item: TestimonialItem, state: { isActive: boolean; index: number }) => React.ReactNode—Replaces the built-in ticker entry. Receives whether that item is the one currently facing forward, so the band can highlight in step with the prism.
tickerClassNamestring""Applied to the ticker band.
renderCard(item: TestimonialItem, state: { isActive: boolean; index: number; offset: number }) => React.ReactNode—Replaces the built-in face entirely. Receives whether this face is forward and how far it is from centre, so you can show less on the ones turned away.
cardBgColorstring"#ffffff"Face colour.
cardBorderRadiusstring"1.25rem"Corner radius of a face, as any CSS length.
cardClassNamestring""Applied to each face.
titlestring"Trusted by Engineering Leaders"Heading above the prism.
titleClassNamestring""Applied to the heading.
backgroundColorstring"#f5f4ef"Colour behind the whole section — a warm off-white by default, not white.
classNamestring""Applied to the wrapper.
styleReact.CSSProperties{}Inline styles merged onto the wrapper.
mount"auto" | "eager""auto"When to start the carousel. The default waits until the page has frame headroom to spare. Set "eager" to start immediately. Note this defaults the opposite way from the components that build a 3D scene: those start eagerly and take "auto" to defer. Both mean the same thing by "auto" — let the runtime pick the moment — the default just matches what each component already did. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference.

<TestimonialItem />

type

One testimonial. Everything beyond the quote and its author is optional, and the face adapts to what you supply.

PropTypeDefaultDescription
id*string—Stable key for the face.
company*string—Company name, also used in the ticker.
quote*string—The testimonial itself. A sentence or two.
authorName*string—Who said it.
authorRole*string—Their title and organisation.
authorAvatar*string—Author photo.
logostring—Company mark shown on the face.
metricValuestring—The headline number, such as "-68%". Shown only when showMetrics is on.
metricLabelstring—What the number refers to, such as "Checkout flow".
ratingnumberdefaultRatingOverrides the default star rating for this item.
accentColorstring—Brand colour used for this item's metric highlight.
It reads the visitor's reduced-motion preference and holds the ticker still. The prism itself still turns when someone asks it to — that is navigation rather than decoration — but nothing moves on its own.
It measures its own container rather than the window, and scales the cards, their spacing and their depth together as one assembly — so it holds its proportions in a panel as well as full bleed. Type, avatars and the ticker step down a size below roughly 750px wide or 500px tall. It fills a parent that has a height and falls back to a 520px floor when the parent does not.
Every face is a 3D-transformed element that stays mounted, so cost follows the number of testimonials rather than what is on screen. A handful is cheap; a long list pays for faces nobody sees.
Client component. It reads the pointer and drives transforms per frame, so it has to run in the browser.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. The marquee's width is measured when it changes rather than every frame. Reading it per frame forced the browser to recalculate layout on every pass, which is the kind of cost that does not show up in a profile as this component's fault — it shows up as the whole page being slow.
Back to all components