# 問題

由於此專案使用 GSAP 而非 framer motion 導致 shadcn/ui 的 accordion 元件在此專案中是沒有展開/收起動畫的,因此需用 GSAP 自行製作動畫效果

# GSAP 動畫建構重點

以下 shadcn/ui 官方的寫法,若專案有 framer motion 應能直接使用

return (
 <Accordion type='single' collapsible className='w-full'>
  <AccordionItem value='item-1'>
   <AccordionTrigger>Is it accessible?</AccordionTrigger>
   <AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
  </AccordionItem>
  ...
 </Accordion>
);

經過思考,得出兩個解決方案:

  1. 第一個版本:完全自定義的 Accordion 實現,使用 GSAP 處理所有動畫效果。
  2. 第二個版本:繼續使用 Shadcn UI 的 Accordion 組件,但通過包裝內容元素和使用 GSAP 增強動畫效果。

為了能以最有效率的方式開發此專案,最終選擇了第二個版本

# 第二個版本

  • 添加了一個包裝 div 並設置 ref,這樣 GSAP 可以直接對其進行動畫處理
  • 使用 onValueChange 事件來觸發 GSAP 動畫
  • 保留了原有的 Shadcn UI 組件結構

程式碼解析:

  • el 是這個 div 元素本身
  • contentRefs.current 是一個物件,它存放了所有 AccordionContent 的 ref
  • 這樣就可以透過 contentRefs.current[service.id] 取得對應的 DOM,用來做動畫。。
return (
    ...
 <div className='space-y-4'>
  {services.map((service) => (
   <Accordion
                ...
    onValueChange={(value) => {
     handleValueChange(service.id, value === service.id);
    }}
   >
    <AccordionItem value={service.id} className='border-none'>
     <AccordionTrigger className='flex items-center space-x-4 p-4 text-lg font-semibold'>
      <div className='flex items-center gap-4'>
       {service.icon}
       <span>{service.title}</span>
      </div>
     </AccordionTrigger>

     <div
      className='gsap-content-wrapper overflow-hidden'
      ref={(el) => {
       contentRefs.current[service.id] = el;
      }}
     >
      <AccordionContent className='p-4 text-gray-600'>{service.content}</AccordionContent>
     </div>
    </AccordionItem>
   </Accordion>
  ))}
 </div>
);

# GSAP 動畫程式碼

幾個重點首先是宣告 contentRefs 來存儲每個 AccordionContent 的 DOM 元素,方便 GSAP 控制動畫效果

const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});

GSAP 動畫處理函數:

  • 展開 → height: "auto", opacity: 1
  • 收起 → height: 0, opacity: 0
  • 使用 power2.in 緩動函數來改善收起效果
// 追蹤 accordion 展開狀態的變化
const handleValueChange = (id: string, isOpen: boolean) => {
 const contentElement = contentRefs.current[id];

 if (contentElement) {
  if (isOpen) {
   // 首先設置初始狀態
   gsap.set(contentElement, {
    height: 0,
    opacity: 0,
    y: -10,
    overflow: "hidden",
   });

   // 然後執行展開動畫
   gsap.to(contentElement, {
    height: "auto",
    opacity: 1,
    y: 0,
    duration: 0.3,
    ease: "power2.out",
    onComplete: () => {
     // 動畫完成後移除高度限制
     gsap.set(contentElement, { clearProps: "overflow" });
    },
   });
  } else {
   // 首先獲取當前高度
   const height = contentElement.offsetHeight;

   // 設置明確的高度(而非 auto)
   gsap.set(contentElement, {
    height: height,
    overflow: "hidden",
   });

   // 然後執行收起動畫
   gsap.to(contentElement, {
    height: 0,
    opacity: 0,
    y: -10,
    duration: 0.3,
    ease: "power2.in",
   });
  }
 }
};

# 成果展示

accordion-animation

請我喝[茶]~( ̄▽ ̄)~*

Young 微信支付

微信支付

Young 支付寶

支付寶