PA02▸🧩 Compose · 互動組合▸折疊
Accordion
折疊面板
難度 ★★★★★標籤 折疊 · FAQ · 互斥 · 動畫高度3 min read
模式簡述
折疊面板看似簡單,雷區其實不少:高度動畫不能直接用 height: auto、互斥模式跟多開模式邏輯不同、
箭頭旋轉要跟內容同步。這裡用 grid-rows-[0fr→1fr] 技巧把高度動畫做得乾淨。
可以,MIT 授權。原作者保留標示權,但不限商用範圍。
全套支援。MDX 在 build 期靜態化,client 端只負責互動。
目前完全免費。未來可能推進階教學 / 一對一付費諮詢。
暫無計畫。React 生態為主,但 prompt 跨框架可移植。
Claude Code Prompt
參考:PA02 accordion 技術:純 React + Tailwind(grid-rows trick) 具體行為: - 預設全部收合 - 點擊 header 展開內容,箭頭旋轉 180 度 - 高度動畫用 grid-rows-[0fr] → grid-rows-[1fr](不用 max-height hack) - 多開模式:state 是 number[],可同時開多個 - 互斥模式:開新的關舊的,state 只保留一個 限制: - 不要用 height: auto 過渡(CSS 不支援,會跳) - 不要用 max-height: 9999px hack(duration 失準) - 動畫 duration 0.3s 剛好,0.5 以上太拖 - 內容 overflow-hidden 必加,不然動畫期間會破版
完整程式碼
'use client'
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
const ITEMS = [
{ q: '可以商業使用嗎?', a: '可以,MIT 授權。' },
{ q: '支援 SSR 嗎?', a: '全套支援。' },
]
export function Accordion({ exclusive = false }) {
const [open, setOpen] = useState<number[]>([])
function toggle(i: number) {
if (exclusive) {
setOpen(open.includes(i) ? [] : [i])
} else {
setOpen(open.includes(i) ? open.filter(x => x !== i) : [...open, i])
}
}
return (
<div>
{ITEMS.map((item, i) => {
const isOpen = open.includes(i)
return (
<div key={i}>
<button onClick={() => toggle(i)}>
{item.q}
<ChevronDown className={`transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
<div className={`grid transition-all duration-300 ${isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}>
<div className="overflow-hidden">
<p>{item.a}</p>
</div>
</div>
</div>
)
})}
</div>
)
}何時用
- ✅ FAQ 區(內容多但不是每題都要讀)
- ✅ 設定頁的分組(同時開多個可比對)
- ✅ Sidebar 的多層導航
何時別用
- ❌ 內容很短(< 2 行),全展開更省事
- ❌ 內容必須讓使用者全部讀過(折疊會被忽略)
- ❌ 表單分段(用 stepper 更清楚進度)
我踩過的坑
-
height: auto過渡無效:CSS 真的不支援 auto 到具體值的過渡。 早期用max-height: 1000px,內容超過 1000 還是被截。最後改grid-rows, 一次解決。 -
箭頭跟動畫不同步:箭頭 transform 用 100ms,內容高度用 300ms, 看起來像兩個元件。統一用 300ms 後順了。
-
互斥模式沒考慮「點同一個」:原本互斥邏輯只寫了「開新關舊」, 但點當前展開的也應該要收起。要先判斷是否是同一個。
相關模式
- 暫無