Frontend Lab
← 回到所有模式
PD09📊 Data Display · 資料展示輪播

Carousel

輪播

難度 ★★★★★標籤 carousel · swipe · indicator3 min read

模式簡述

Carousel:水平捲動的圖片或卡片列。電商商品、文章推薦。手機端要支援 swipe。

幻燈片一
幻燈片二
幻燈片三
幻燈片四
幻燈片五
第 1 張|首張 — 上一張 disabled

Claude Code Prompt

💬 Claude Code Prompt
參考:PD09 carousel
組件:Carousel.tsx

Carousel:水平捲動的圖片或卡片列。電商商品、文章推薦。手機端要支援 swipe。

限制:
- 沒做手機 swipe
- 自動播放沒暫停
- indicator 只顯示位置不可點

完整程式碼

Carousel.tsx
"use client";
import { useEffect, useState } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";

type State = "first" | "middle" | "last";

const SLIDES = [
{ color: "from-lab-accent to-pink-500", title: "幻燈片一" },
{ color: "from-blue-500 to-cyan-500", title: "幻燈片二" },
{ color: "from-emerald-500 to-teal-500", title: "幻燈片三" },
{ color: "from-purple-500 to-pink-500", title: "幻燈片四" },
{ color: "from-orange-500 to-yellow-500", title: "幻燈片五" },
];

export function Carousel({ state = "first" }: { state?: State }) {
const idx = state === "first" ? 0 : state === "middle" ? 2 : SLIDES.length - 1;
const [active, setActive] = useState(idx);
useEffect(() => setActive(idx), [idx]);

return (
  <div className="w-full max-w-md space-y-2">
    <div className="relative overflow-hidden rounded-lg border border-border">
      <div
        className="flex transition-transform duration-500 ease-out"
        style={{ transform: `translateX(-${active * 100}%)` }}
      >
        {SLIDES.map((s, i) => (
          <div
            key={i}
            className={`grid h-44 w-full shrink-0 place-items-center bg-gradient-to-br ${s.color}`}
          >
            <span className="text-lg font-medium text-white drop-shadow">{s.title}</span>
          </div>
        ))}
      </div>
      <button
        onClick={() => setActive((a) => Math.max(0, a - 1))}
        disabled={active === 0}
        className="absolute left-2 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-full bg-background/70 backdrop-blur disabled:opacity-30"
      >
        <ChevronLeft className="h-4 w-4" />
      </button>
      <button
        onClick={() => setActive((a) => Math.min(SLIDES.length - 1, a + 1))}
        disabled={active === SLIDES.length - 1}
        className="absolute right-2 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-full bg-background/70 backdrop-blur disabled:opacity-30"
      >
        <ChevronRight className="h-4 w-4" />
      </button>
    </div>
    <div className="flex justify-center gap-1.5">
      {SLIDES.map((_, i) => (
        <button
          key={i}
          onClick={() => setActive(i)}
          className={cn(
            "h-1.5 rounded-full transition-all",
            i === active ? "w-6 bg-lab-accent" : "w-1.5 bg-muted-foreground/40",
          )}
        />
      ))}
    </div>
  </div>
);
}

何時用

  • ✅ 商品 gallery
  • ✅ 首頁推薦內容區
  • ✅ feature 比較

何時別用

  • ❌ 首屏 hero(自動播放害無障礙閱讀)
  • ❌ 重要資訊(使用者根本不會等到看完)
  • ❌ 資料超過 20 項(用 grid 或 list)

我踩過的坑

  1. 沒做手機 swipe:桌面好用但手機點箭頭很彆扭。用 framer-motion drag。

  2. 自動播放沒暫停:滑鼠 hover 應該暫停,至少要有手動暫停。

  3. indicator 只顯示位置不可點:使用者想跳到第 5 張要按 5 次。dot 要可點。

相關模式

  • PD11
  • PD12

搜尋

按 ⌘K 隨時開啟