動畫分層與懸停配方
兩個在 17 個原型應用中驗證過的概念:
- 持續時間是一個尺度,而非每個元件的獨立數字。 眼睛將統一的節奏視為同一個產品。愈小的元件移動得愈快。
- 懸停柔和地移動幾何形狀。 一個逐漸填滿整行的高亮、一個抵達的箭頭、一個傾向於其去向的 V 形指示器、一個滾動至下一個狀態的標籤。僅有顏色變化的懸停看起來像在切換開關。
檔案
references/recipes.css:Tailwind v4 的@theme符號與@utility配方。references/timing.ts:適用於 Motion 與 WAAPI 的同一組分層。references/swap.tsx:IconSwap與LabelSwap(React)。
分層
| 分層 | 毫秒 | 適用場景 |
|---|---|---|
tap | 100 | 圖示互換、按下動作 |
pop | 150 | 選單、彈出視窗或工具提示開啟 |
hover | 200 | 回應游標的列或控制項 |
card | 300 | 卡片抬升、列表穩定、頁面切換 |
layout | 500 | 面板收起、頁面重新排列 |
- 進入時使用緩出曲線(
cubic-bezier(0.22,1,0.36,1))。退出時使用緩入曲線,且持續時間縮短一層。 - 當兩個元素互換位置時,移出的元素立即離開,移入的元素在
delay-handoff(50 毫秒)後開始。 - 數值補間(計數器、進度條、圖表線條繪製)可持續更長時間,因為眼睛在跟隨資料。
- 保留刻意使用的彈簧效果。
遷移方式:將每個隨意定義的持續時間對應到最接近的分層。
| 隨意定義的值 | 分層 |
|---|---|
| 0.12 | tap |
| 0.15–0.18 | pop |
| 0.2–0.24 | hover |
| 0.28–0.35 | card |
| 0.4–0.5 | layout |
預期螢幕上顯示的時序幾乎在所有地方變化不超過 20 毫秒。
配方(每個元素一個)
hover-grow(導航列、連結列):- 高亮是一個
::before,初始時向內縮排(3px 6px)且透明,然後在淡入的同時擴展至整列。 - 對於
:focus-visible、data-state=open與aria-current=page也會顯示。 - 正方形或粗獷主義設計(類似 Carbon)應改用平面高亮。
- 高亮是一個
trail(連結列、「瀏覽方案」):- 一個尾隨字形,起初是半透明且縮小一半,直到父級
.group被懸停或聚焦時,從其前緣開始放大。 trail-out(從左下角放大)加上arrow-up-right標記離開應用的連結。
- 一個尾隨字形,起初是半透明且縮小一半,直到父級
nudge/nudge-back(V 形指示器、「檢視全部」、返回連結):朝目標方向傾斜 2px。lift(可點擊卡片):- 表面提高一個亮度層級,陰影加深一個層級,持續時間為
card層級。 - 禁止使用 translate:在游標下移動的卡片會干擾瞄準。
- 表面提高一個亮度層級,陰影加深一個層級,持續時間為
scroll-fade(滾動面板):- 基於遮罩的邊緣淡出,僅在可滾動的一側顯示。由容器自身的滾動時間線驅動,不需要背景顏色。
- 不支援的瀏覽器則不會顯示淡出效果。
IconSwap(選單轉關閉、複製轉勾選、太陽轉月亮):- 移出的字形縮小、傾斜 −22° 並以緩入曲線淡出。
- 移入的字形在交接延遲後進入。
- 兩者保持掛載狀態,以便在過渡過程中順利反向。
LabelSwap(「複製連結」轉「已複製」、「收起」轉「展開」):- 兩個標籤位於同一個網格儲存格中,因此不會有寬度跳動。
- 隱藏的標籤透過
transition-discrete設為invisible,因此輔助技術只能聽到當前的標籤。
陷阱
- 對自身有動畫的元素使用
scroll-fade(Radix 選單、彈出視窗或選取內容)會取代該元素的退出動畫。Radix 會等待一個永不結束的退出,導致選單保持開啟。請將淡出效果放在其內部的滾動子元素上。 scroll-fade也會遮罩邊框和陰影。 不要將其放在有邊框的卡片上,應放在內部的滾動容器上。- Tailwind v4 中的自訂
duration-*工具必須同時設定--tw-duration,否則同一元素上的transition-[…]工具會根據排序順序勝出。 - 帶有行高的具名型別步驟(
--text-meta--line-height)在替換繼承了父級前導的任意尺寸時,會使每行轉換後偏移一個像素。中間步驟不應設定自己的行高。 - 將懸停效果限制在
@media (hover: hover)內。 在:focus-visible上顯示相同的狀態,並尊重prefers-reduced-motion(Motion:<MotionConfig reducedMotion="user">)。