3D Tilt Review Card
A slightly askew testimonial card that tilts in 3D perspective with a moving sheen as the pointer moves, the quote floating above the surface, flipping over to switch entries.
A testimonial section that tells one customer story at a time. The centred card rests at a slight angle; move the pointer over it and it tilts in 3D perspective based on the pointer position, with a sheen that follows the pointer across the surface. The big quote mark, the quote and the author row float above the card along the Z axis, while the blurred blobs and fine grid behind it shift the opposite way to add depth. Switching with the buttons below turns the card edge-on, swaps in the next entry and turns it back, in the direction of "previous" or "next". The current index supports 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 3d-tilt-review-card.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ThreeDTiltReviewCard/>
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/3d-tilt-review-card.en.jsonOr, once registries are configured in components.json, install it as @webberui/3d-tilt-review-card.
Usage
import { ThreeDTiltReviewCard } from "@/components/ui/3d-tilt-review-card";
<ThreeDTiltReviewCard
eyebrow="客戶怎麼說"
title="一張卡,說一個真實的改變"
testimonials={[
{
quote: "換上新的點餐頁之後,尖峰時段的出杯速度明顯快了一截。",
name: "林語晴",
role: "營運經理",
company: "青嶼咖啡",
rating: 5,
highlight: "出杯速度明顯快了一截",
metric: { value: "+32%", label: "尖峰出杯量" },
},
{
quote: "新進同仁第一天就能自己跑完整條路線。",
name: "周柏翰",
role: "調度主任",
company: "北辰物流",
},
]}
maxTilt={10}
restRotate={-3}
onIndexChange={(i) => console.log(`切到第 ${i + 1} 則`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
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 |
testimonials | ThreeDTiltReviewCardItem[] | — | Testimonials, shown one at a time |
maxTilt | number | 10 | Maximum tilt in degrees, reached at the card edge; 0 disables tilt |
restRotate | number | -3 | Resting in-plane tilt (degrees); the --wb-3d-tilt-review-card-rest CSS variable wins |
index | number | — | Controlled current index; managed internally when omitted |
defaultIndex | number | 0 | Initial index in uncontrolled mode |
onIndexChange | (index: number) => void | — | Called when the index changes |
glare | boolean | true | Show the pointer-following sheen on the card face |
stiffness | number | — | Tilt spring stiffness; reads --wb-3d-tilt-review-card-stiffness when omitted (default 200) |
duration | number | — | Total flip duration (ms); reads --wb-3d-tilt-review-card-duration when omitted (default 500ms) |
prevLabel | string | "上一則評價" (Previous testimonial) | Accessible name of the "previous" button |
nextLabel | string | "下一則評價" (Next testimonial) | Accessible name of the "next" button |
className | string | — | Class added to the root section |
ThreeDTiltReviewCardItem
| Field | Type | Default | Description |
|---|---|---|---|
quote | string | — | Testimonial text |
name | string | — | Reviewer's name; the avatar shows initials (last two characters for CJK, first letters for Latin) |
role | string | — | Job title |
company | string | — | Company or organization |
rating | number | — | Star rating (1–5); hidden when omitted |
avatarHue | number | — | Avatar gradient hue (0–360); assigned by index with the golden angle when omitted |
highlight | string | — | Fragment of the quote to mark with the accent colour (must match exactly) |
metric | { value: string; label: string } | — | Result metric to the right of the author row |
How it works
- Timing variables:
--wb-3d-tilt-review-card-stiffness(tilt spring, default 200, damping fixed at 20),--wb-3d-tilt-review-card-duration(total flip time, default 500ms, split evenly between exit and enter) and--wb-3d-tilt-review-card-rest(resting tilt, default -3deg). The accent colour comes from--wb-3d-tilt-review-card-accent(#0d9488 light, #2dd4bf dark). - Layers: the big quote mark sits at
translateZ(60px), the quote and author row attranslateZ(40px); the card itself does not useoverflow-hidden, so the 3D is not flattened. - The sheen is a 15% white radial-gradient whose centre follows the pointer; when the pointer leaves, tilt and sheen spring back to rest with the same spring.
- Flip direction: "next" turns to 90° and comes back from -90°, "previous" does the reverse; wrapping from the last entry to the first still counts as "next".
- Touch devices (
pointer: coarse) skip pointer tilt and keep only the resting tilt and the flip.
Accessibility
- The root is a
<section>; with atitleit usesaria-labelledbypointing at the heading, otherwise the eyebrow or "Testimonials" becomes itsaria-label - Each entry renders as
<figure>/<blockquote>/<figcaption>; the star rating is announced throughrole="img"with a text label - Switch buttons are
type="button"with anaria-labeland a focus-visible ring; the left and right arrow keys switch entries within the button group - On switch, a visually hidden
aria-live="polite"region announces the position ("2 of 4") and the quote; the two cards that briefly coexist mid-flip are not announced twice - When the system's reduce motion setting is on, pointer tilt, sheen and flip are disabled and switching becomes a 150ms crossfade; entries can still be switched with the buttons
Autoplay Quote Card
One large centred testimonial card that autoplays, with a bottom progress bar counting down to the next quote; content slides and fades between entries, with pause and manual controls.
Quote & Result Columns
Static testimonial columns React component, each with a key quote and a results figure; on entry the dividers draw down, the figures count up from zero and the columns appear in sequence, under an optional generative contour backdrop.