Frontend Lab
← 回到所有模式
PG06📐 Layout · 版型見證

Animated Testimonials

動態見證牆

難度 ★★★★★標籤 testimonial · carousel · Framer3 min read

模式簡述

Animated Testimonials:見證的動態切換。圖片 fade + scale + rotate,文字滑入滑出。比靜態 testimonial 有質感。

"最完整的前端模式收藏。"

陳冠廷

前端工程師

第 1 則|首則 — 自動入場動畫

Claude Code Prompt

💬 Claude Code Prompt
參考:PG06 animated-testimonials
組件:AnimatedTestimonials.tsx

Animated Testimonials:見證的動態切換。圖片 fade + scale + rotate,文字滑入滑出。比靜態 testimonial 有質感。

限制:
- 動畫做完太快
- 沒做 reduced-motion
- 只有自動播放沒控制

完整程式碼

AnimatedTestimonials.tsx
"use client";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";

type State = "first" | "second" | "third";

const Q = [
{ name: "陳冠廷", role: "前端工程師", quote: "最完整的前端模式收藏。", color: "from-rose-400 to-orange-500" },
{ name: "Sarah Kim", role: "Design Engineer", quote: "Apple 系列動效救了我們的 onboarding。", color: "from-emerald-400 to-teal-500" },
{ name: "Yuki Tanaka", role: "Indie Hacker", quote: "從 hero 到 footer 全有,landing 半天搞定。", color: "from-blue-400 to-violet-500" },
];

export function AnimatedTestimonials({ state = "first" }: { state?: State }) {
const idx = state === "second" ? 1 : state === "third" ? 2 : 0;
const [active, setActive] = useState(idx);
useEffect(() => setActive(idx), [idx]);

const t = Q[active];

return (
  <div className="w-full max-w-xl">
    <div className="grid grid-cols-[120px_1fr] gap-5 rounded-xl border border-border bg-background/30 p-5">
      <div className="relative h-32 w-30">
        <AnimatePresence mode="wait">
          <motion.div
            key={active}
            initial={{ opacity: 0, scale: 0.92, rotate: -4 }}
            animate={{ opacity: 1, scale: 1, rotate: 0 }}
            exit={{ opacity: 0, scale: 0.92, rotate: 4 }}
            transition={{ duration: 0.35 }}
            className={`grid h-full w-full place-items-center rounded-xl bg-gradient-to-br ${t.color}`}
          >
            <span className="font-mono text-2xl text-white/80">{t.name[0]}</span>
          </motion.div>
        </AnimatePresence>
      </div>
      <div className="flex flex-col justify-between">
        <AnimatePresence mode="wait">
          <motion.div
            key={active}
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            transition={{ duration: 0.25 }}
          >
            <p className="text-sm italic leading-relaxed text-foreground">"{t.quote}"</p>
            <p className="mt-3 text-xs font-medium">{t.name}</p>
            <p className="font-mono text-[10px] text-muted-foreground">{t.role}</p>
          </motion.div>
        </AnimatePresence>
        <div className="flex gap-2">
          <button
            onClick={() => setActive((a) => (a - 1 + Q.length) % Q.length)}
            className="grid h-7 w-7 place-items-center rounded-full bg-muted hover:bg-lab-accent/15"
          >
            <ChevronLeft className="h-3 w-3" />
          </button>
          <button
            onClick={() => setActive((a) => (a + 1) % Q.length)}
            className="grid h-7 w-7 place-items-center rounded-full bg-muted hover:bg-lab-accent/15"
          >
            <ChevronRight className="h-3 w-3" />
          </button>
        </div>
      </div>
    </div>
  </div>
);
}

何時用

  • ✅ 高質感 marketing site
  • ✅ 需要展示多位客戶但空間有限
  • ✅ Apple 風格的 brand site

何時別用

  • ❌ B2B 嚴肅介面(過於華麗)
  • ❌ 見證少於 3 個(直接並排)
  • ❌ 效能敏感(無限重渲染)

我踩過的坑

  1. 動畫做完太快:500ms 內感覺像 glitch。要 700-900ms。

  2. 沒做 reduced-motion:暈動敏感者頭暈。respect prefers-reduced-motion。

  3. 只有自動播放沒控制:使用者想暫停或往回看沒辦法。要左右箭頭 + 點 indicator。

相關模式

  • PG04
  • PD11

搜尋

按 ⌘K 隨時開啟