motion 是做什麼的?
此技能負責建立動畫。它擴充了 product-ui/references/values.md 中的動畫章節,該檔案存放已確定的數值(緩動曲線、按壓縮放、彈跳、錯開、持續時間表格、彈出通知與版面配置持續時間、移動距離、退出與進入比例)。此處不重複這些數值;此技能加入了決策順序、權杖、配方與失敗模式。
在任何決策之前,請先套用 product-ui §1(品味日誌與優先順序)與 §5(哪個技能負責什麼)。
決策順序
依序從上到下進行。每個步驟都可以結束任務。零行動畫是有效的結果,請直接說明。
1. 該動嗎?頻率閘門
詢問一位使用者每天看到這個動畫的次數。分級標準在 values.md 中。實務上:
- 由鍵盤觸發,或每天看到 100 次以上:不加入動畫。原因:重複的動畫會變成延遲。
- 每天看到幾次(懸停、分頁切換、行選取):不加入動畫,或僅做即時的色彩變化。
- 使用者正在閱讀或操作的資料(表格中的數字、即時儀表板中的圖表)絕不會為了風格而動。原因:動畫會將視線從正在閱讀的內容上拉開。
- 如果閘門判定為「否」,則不寫入動畫。說明它命中了哪個分級,並提供靜態替代方案(即時的狀態變更)。
2. 命名目的
選擇一個詞:空間(從哪裡來或到哪裡去)、狀態(此處已變更)、回饋(我聽到了)、連續性(防止跳躍)、說明(僅在罕見時刻)。如果無法命名一個詞,則停止。原因:頻繁元素上的「看起來不錯」是造成 UI 感覺遲緩的主要原因。
3. 選擇工具:最便宜且有效的那個
| 需求 | 工具 |
|---|---|
| 由 class、屬性、懸浮或按壓切換的狀態 | CSS transition |
從 display: none、彈出視窗或對話框進入 | @starting-style + transition-behavior: allow-discrete |
| 一次性序列、錯開,或在頁面忙碌時必須執行的動畫 | CSS @keyframes |
JS 決定何時開始;需要取消、反向、finished 或 FLIP | WAAPI el.animate() |
| Spring、速度傳遞、卸載中 React 節點的離開、共享版面配置 | Motion (motion/react) |
如果是整個元件(選單、彈出通知、抽屜),請使用內建焦點處理的函式庫,並呼叫 pick-library。切勿為了淡入效果而安裝 Motion。原因:CSS 在主執行緒外執行,且不增加套件大小。
4. 屬性
- 對
transform和opacity設定動畫;filter僅用於 ≤ 2px 的交換模糊。命名每個屬性;絕不使用transition: all。 - 高度和版面配置變更:請參閱
references/recipes.md(網格行、FLIP)。切勿設定width、top、left或margin的動畫來移動元素。原因:每個都會在每一幀強制進行版面配置。 - 切勿設定
box-shadow的動畫。請改為淡入偽元素的opacity。 translate百分比是相對於元素自身大小,因此translateY(100%)適用於任何高度的抽屜和彈出通知。
5. 曲線與持續時間,依用途配對
切勿選擇最接近你猜測的數值。先說明用途(「選單關閉」、「圖示交換」),然後從 references/tokens.md 中選取對應的行。如果沒有與用途相符的行,請新增一個以該用途命名的權杖;不要硬套現有的權杖。原因:權杖是對動畫用途的陳述,因此用途不對的近似值就是錯誤的權杖。
- 進入或離開:
--ease-out。在螢幕上移動:--ease-in-out。抽屜:--ease-drawer。色彩:ease。旋轉圖示:linear。絕不使用ease-in。 - 任何可拖曳或在動作途中可中斷的元素應使用 spring,而非曲線。原因:曲線無法承接使用者手部產生的速度。
- 感覺遲緩:先縮短持續時間,再減少移動距離,最後才調整曲線。
6. 中斷
- 使用者可以在一秒內觸發兩次的任何項目,都應使用 transition 或 spring,絕不使用
@keyframes。原因:transition 會從當前值重新定位;keyframes 會重新啟動並跳躍。 - WAAPI:在讀取當前值後取消舊動畫,並從該值開始新的動畫(
references/recipes.md)。 - 一個切換開關被快速點擊 10 次,必須在正確的狀態結束,且無跳躍。請測試它。
7. 離開
- 預設離開是淡出,且比進入更短(權杖檔案中有各組配對)。
- 任何有方向的元素(抽屜、面板、滑動關閉的彈出通知)應以原路離開。
- 離開的元素會立即停止佔用空間,因此兄弟元素會在它淡出時填補空隙。
- 離開動作是同時進行的,絕不錯開。原因:使用者已經決定要離開了。
8. 減少動畫,與動畫一同提供
保留不透明度和色彩。滑動、縮放、模糊和彈跳移除;滑動變為淡出,彈跳變為 0。切勿將動畫作為唯一的提示。模式請參閱 references/recipes.md。
編排
在計時之前,先為每個移動元素分配角色:
| 角色 | 處理方式 |
|---|---|
| 主角(唯一需要關注的事物) | 最先移動,移動距離最遠 |
| 配角 | 跟隨移動,移動距離較短 |
| 裝飾 | 原地淡入,或不移動 |
| 靜態(容器、瀏覽器外殼) | 絕不移動 |
- 每個容器只有一次進入。如果面板滑入,其內容應已存在。原因:兩層進入動畫會讓使用者等待兩次。
- 順序以重要性為準,而非 DOM 順序。延遲保持在 values.md 的步驟(項目之間 40ms,標題、文字和動作之間 100ms)。透過角色而非延遲來變化移動距離和持續時間。
- 列表錯開在第 6 項達到上限(最後一項延遲 240ms);之後的項目共用此延遲。在動畫播放期間,輸入功能仍需正常運作。
- 絕不錯開選單、指令面板、分頁內容或任何重複開啟的項目。
- 保持靜止:使用者即將點擊的任何項目、頁面再次載入時已在螢幕上的任何項目,以及距離上一次更新在 150ms 內的更新(直接跳到結束狀態)。
- 方向應符合意義:下一項從右側進入,返回從左側進入;使用者觸發的事物從觸發點向外展開。
需要時才載入
- references/tokens.md:當需要確切的持續時間、距離、縮放、模糊、緩動或 spring 值,以及為現有程式碼庫建立權杖時載入。
- references/recipes.md:當建置選單、彈出視窗、對話框、抽屜、彈出通知、列表進入、圖示交換、高度變更、分頁指示器或數字變更時載入。
- references/gestures.md:當需要拖曳、滑動、輕彈、面板,或任何跟隨指標的事物時載入。
- references/performance.md:當動畫掉幀時,在動用
will-change之前,以及當更新快速到來時載入。
配對範例
| 錯誤寫法 | 修正寫法 | 原因 |
|---|---|---|
.menu { transition: all 300ms ease-in } | transition: opacity 200ms, transform 200ms var(--ease-out) | 命名屬性;ease-in 感覺遲緩;選單 ≤ 200ms |
scale(0) 到 scale(1) | scale(0.95) 和 opacity: 0 到 1 | 沒有東西是從虛無中出現的 |
| 彈出視窗從中心縮放 | transform-origin: var(--transform-origin) | 它應該從它的觸發點向外生長 |
.open { animation: slide-in 300ms } | transition: transform 在切換的狀態上 | Keyframes 在中斷時會快照跳躍 |
每一行 animation-delay: i * 120ms | min(i, 6) * 40ms | 最後一行在 300ms 內到達 |
| 抽屜在滑入後淡出 | 它會沿原路滑出 | 方向本身就是一種解釋 |
拖曳放開時使用固定的 ease-out | 使用帶有釋放速度的 spring | 手的動量會持續 |
在 pointermove 中使用 setY(y) | el.style.transform = ... | 避免每幀重新渲染 |
完成前
搜尋 diff,然後驗證。
| 搜尋內容 | 修正方式 |
|---|---|
transition: all、* 上的 will-change、ease-in | 使用命名屬性、移除、改為 --ease-out |
彈出視窗上的 scale(0)、transform-origin: center | 改為 0.95、觸發點的原點(對話框保持置中) |
在一個會切換的 class 上的 @keyframes | 改為 transition |
| 持續時間超過 tokens.md 中對應行,或某個數值不在 tokens.md 中 | 使用該用途的權杖 |
keydown 處理器或指令面板上的動畫 | 移除它 |
transition 中的 width、height、top、left、box-shadow | 改為 transform、opacity,或依照配方 |
沒有 prefers-reduced-motion 路徑 | 新增它 |
在執行中的應用程式裡驗證:以 4 倍速播放(開發者工具動畫面板,或將持續時間乘以 4),快速點擊任何切換開關,在開啟減少動畫的情況下測試,並將 CPU 節流至 4 倍。說明哪些測試你無法執行。