Apple 的介面設計與流暢、物理性動畫方法,轉化為網頁實作。在建立或審查手勢驅動 UI、彈簧動畫、拖曳/滑動/面板互動、動量與可中斷轉場、半透明材質與深度、字體排印(光學尺寸、字距、行距)、減少動畫,或 Apple 風格介面背後的設計基礎(回饋、空間一致性、克制)時使用。
Apple Design
Apple 如何打造不再像電腦、而像你身體延伸的介面。這些知識來自 Apple 的 WWDC 設計講座——主要是《Designing Fluid Interfaces》(WWDC 2018)——經過提煉並轉化為網頁平台(CSS、Pointer Events、requestAnimationFrame、Motion/Framer Motion 等彈簧函式庫)。
主線:當動畫從螢幕上的當前數值開始、繼承使用者的速度、向前投射動量、並且可以在任何瞬間被抓住並反向時,介面就有了生命力。彈簧是讓這一切變得自然的工具,因為它們本質上可中斷且能感知速度。
核心概念
「當我們讓介面貼合我們的思考與動作方式時,神奇的事就會發生——它不再像電腦,而開始像我們無縫的延伸。」
當介面像物理世界一樣運作時,它就是流暢的:即時回應、連續移動、帶有動量、在邊界處產生阻力、並且可以在移動中途被重新導向。以下所有內容都是為了更接近這個目標。
Apple 將設計定位為滿足四種人類需求:安全感/可預測性、理解、成就、喜悅。 這裡的每一條規則都服務於其中之一。
1. 回應——消除延遲
一旦出現延遲,直接操作的感覺就會「斷崖式下降」。回應是所有其他一切的基礎。
- 在手指按下時回應,而不是放開時。 按下的瞬間就讓按鈕產生視覺變化。等到
click/手指放開才顯示回饋,感覺就像死了一樣。 - 警惕每一毫秒的延遲。 檢查 debounce、人工計時器、轉場等待時間,以及約 300ms 的點擊延遲。任何輸入路徑上非必要的東西都是退步。
- 回饋必須在互動過程中持續提供,而不只是在結束時。 對於拖曳、滑桿或抽屜,整個過程中都要讓 UI 與手指 1:1 同步更新——千萬不要只在手勢完成時才動畫。
/* 回饋存在於按下的瞬間,而且即時生效 */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}
2. 直接操作——1:1 追蹤
「觸碰與內容應該一起移動。」
當使用者拖曳某個東西時,它必須緊貼手指——並且要考慮抓取時的偏移量。如果抓取時直接對齊元素中心,會立刻破壞沉浸感。
- 使用 Pointer Events 搭配
setPointerCapture,這樣即使指標離開元素邊界,追蹤仍能持續。 - 記錄短暫的速度/位置歷史(最近幾次
pointermove事件),而不只是當前點——你需要在放開時知道速度。
el.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // 尊重抓取位置
// ...記錄位置與時間戳歷史以計算速度
});
3. 可中斷性——最重要的原則
「想法與手勢是同步發生的。」
每個動畫都必須能在任何時刻被中斷並重新導向。使用者必須能在移動中的元素飛行途中抓住它並反向移動,而不需要等動畫結束。一個正在關閉的模態視窗,如果使用者再次抓住它,應該要跟著手指移動——而不是先關閉完成再重新開啟。
- 絕不在轉場期間鎖定輸入。
- 永遠從當前的顯示數值開始動畫,而不是目標數值。 中斷時,讀取元素即時的螢幕 transform 數值,並從那裡開始新的動畫。從邏輯/目標數值開始會導致明顯的跳躍。
- 避免在手勢驅動的互動中使用 CSS transitions 和
@keyframes——它們無法在飛行途中被順暢地抓住並反向。彈簧預設從當前數值開始動畫,這正是中斷所需要的。 - 當手勢反向時,要混合速度,而不是硬切。 在反向點用一個動畫取代另一個會造成速度不連續,就像撞到「磚牆」。能夠在重新設定目標時保留速度的彈簧函式庫可以避免這個問題。(這就是 iOS 的 additive animations 原生做到的事;在網頁上,選擇一個能在重新設定目標時從當前速度繼續的彈簧函式庫。)
- 將 2D 運動分解為獨立的 X 和 Y 彈簧。 如果 X 和 Y 有不同的速度,單一彈簧作用在 2D 距離上會導致不同步。
4. 行為重於動畫——使用彈簧
「把動畫想成你與物體之間的對話,而不是介面預先寫好的腳本。」
預先編寫、固定時長的動畫無法回應新的輸入。彈簧可以——新的輸入只是改變目標,而運動保持連續。任何使用者可以觸碰的東西都應該使用彈簧。
Apple 刻意將物理三參數(質量/剛度/阻尼)替換為兩個設計師友善的參數。用這些來思考:
- 阻尼比——控制 overshoot。
1.0= 臨界阻尼,沒有彈跳,平穩到達。< 1.0= 超過目標並震盪。數值越低越有彈性。 - 回應時間——數值到達目標的快慢,單位為秒。數值越低越靈敏。這不是「持續時間」——彈簧沒有固定持續時間;它的穩定時間是由參數自然產生的。
預設值:
- 大多數 UI 從阻尼
1.0(臨界阻尼)開始——優雅且不分散注意力。 - 只有在手勢本身帶有動量時(例如輕彈、拋擲、拖曳放開)才加入彈跳(阻尼約
0.8)。一個只是淡入的選單出現 overshoot 感覺不對;但你輕彈出去的卡片出現 overshoot 就感覺很自然。
Apple 實際使用的數值:
| 互動 | 阻尼 | 回應時間 |
|---|---|---|
| 移動/重新定位(例如子母畫面) | 1.0 |
0.4 |
| 旋轉 | 0.8 |
0.4 |
| 抽屜/面板 | 0.8 |
0.3 |
網頁對應(Motion / Framer Motion): bounce + duration 的彈簧 API 與 Apple 的阻尼 + 回應時間非常接近。一個安全的團隊風格是預設到處使用 damping: 1.0 的彈簧;保留彈跳給由動量驅動的物理互動。
import { animate } from 'motion';
// 臨界阻尼預設(無 overshoot)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// 動量互動——稍微彈跳,只因為前面有輕彈動作
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. 速度交接——拖曳與動畫之間的無縫銜接
當手勢結束時,動畫必須以手指的準確速度繼續,這樣拖曳與動畫之間就不會有可見的接縫。這是區分「流暢」與「還不錯」的最關鍵細節。
將指標放開時的速度作為彈簧的初始速度傳入。有些彈簧 API 需要相對速度——將其除以到目標的剩餘距離來正規化:
relativeVelocity = gestureVelocity / (targetValue − currentValue)
範例:元素在 y=50,目標 y=150(還有 100px),手指移動速度 50px/s → 初始彈簧速度 = 50 / 100 = 0.5。Framer Motion / Motion 直接接受絕對 px/s 速度(velocity 選項),所以你通常直接傳入原始數值。
6. 動量投射——動畫到手指「要去」的位置
「用小輸入產生大輸出。」
不要從放開點直接跳到最近的邊界。使用速度來投射最終停留位置——就像滾動減速一樣——然後跳到離投射點最近的目標。這就是讓輕彈感覺像把元素丟出去的原因。
Apple 的準確投射函數(來自《Designing Fluid Interfaces》的範例程式碼):
// decelerationRate ≈ 0.998 為一般滾動感;0.99 為更靈敏
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // 從投射點選擇目標
animateSpringTo(target, { velocity: releaseVelocity }); // 然後交接速度(§5)
注意:物理課本中的 v²/(2·decel) 不是 Apple 使用的公式——請使用上述指數衰減形式。這是好的底部面板和輪播(Vaul、Embla)的標準行為。
7. 空間一致性——對稱路徑、固定錨點
「如果某個東西以某種方式消失,我們會預期它從原來的地方出現。」
- 進出應沿同一路徑。 從右側滑入的面板必須向右側關閉。從右進、從底出會讓人感到脫節與困惑。
- 將互動錨定到其來源。 選單、彈出視窗或面板應該從觸發它的元素處產生——將
transform-origin設為觸發元素,這樣按鈕與內容之間的空間關係就很明顯。(這與彈出視窗從觸發點縮放、而不是從中心縮放的概念相同。) - 在可逆轉場中鏡像 easing 曲線,使離開路徑與返回路徑一致(對兩個方向使用反向的 cubic-bézier 控制點)。
8. 在手勢方向上給予提示
人類會從軌跡預測最終狀態。中間的運動應該暗示事物將往哪裡去——控制中心模組「朝你的手指方向向上向外生長」。讓中間幀指向結果,而不只是盲目地內插到目標。
9. 橡皮筋效果——軟邊界
在邊界處,應逐漸增加阻力,而不是硬生生停止。硬停止感覺像「凍結」;持續的阻力感覺像「有回應,但這裡沒有更多東西了」。應用阻尼,當使用者拖曳超過邊界越遠時,阻尼越大。
// 超過邊界越遠,元素跟隨得越少——真實物體在停止前會減速
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}
10. 手勢設計細節(「手感」檢查清單)
- 點擊: 在觸碰按下時(立即)顯示高亮,在觸碰放開時確認。在目標周圍加入約 10px 的遲滯/點擊填充,並允許透過拖曳離開再回來取消。
- 拖曳/滑動: 在確定方向前需要一個小的移動閾值(遲滯,約 10px),然後進行 1:1 追蹤。
- 從第一次移動就平行偵測所有可能的手勢,然後在意圖明確時自信地取消失敗者。避免只報告最終狀態的辨識器(例如
swipeleft類型的事件)——它們會丟棄你需要的連續追蹤資訊來提供回饋。 - 最小化辨識延遲。 雙擊偵測無可避免地會延遲單擊;只有在確實存在雙擊的地方才付出這個代價。
11. 幀級流暢度
流暢度關乎幀的內容,而不只是幀率。
- 保持每幀的位置變化低於感知閾值,以避免閃爍。
- 對於非常快的運動,微妙的動態模糊/拉伸可以編碼速度,比銳利的硬線條更容易閱讀。
requestAnimationFrame是網頁上與顯示器同步的時鐘(Apple 使用CADisplayLink)。只對合成器友善的屬性進行動畫——transform和opacity——並在即將發生動畫的地方使用will-change提示。
12. 材質與深度——半透明傳達層級
Apple 使用半透明材質作為浮動的功能層,帶來結構感而不搶走焦點。在網頁上,可以用 backdrop-filter 近似模擬。
- 將導航欄/工具欄/面板建構為半透明層(
backdrop-filter: blur()+ 半透明背景),內容在其下方滾動——而不是佔據固定區域的不透明條。 - 材質重量傳達層級: 較暗/較重的材質區分結構區域(側邊欄);較輕的材質將注意力吸引到互動元素(按鈕)。絕不要將一個淺色半透明表面疊在另一個上面——可讀性會崩潰。
- 較大的表面應感覺較厚: 比小晶片更強的模糊和更深的陰影。考慮情境感知的陰影——在繁忙/文字內容上使用較重的陰影以增加分離感,在簡單背景上使用較輕的陰影。
- 變暗以集中焦點,分離以保持流暢。 一個模態任務會將表面與變暗的遮罩結合,並將背景推後/向下。一個平行、非阻塞的面板使用半透明和偏移而不加遮罩,這樣流程就不會中斷。對於堆疊的面板,逐漸變暗並將每個父層向後推。
- Vibrancy 讓文字在變化的背景上保持可讀。 在模糊/半透明表面上,不要使用純灰色文字——使用更高對比度、稍重一點的字重,並稍微增加字距。將顏色放在實色層上,而不是半透明前景上。
- 使用滾動邊緣效果,而不是硬分隔線。 與其在固定標題下使用 1px 邊框,不如在內容與浮動 chrome 重疊處淡出一個小的模糊/漸層遮罩——只在浮動 UI 實際重疊內容的地方使用。
- 材質化,而不只是淡入淡出。 對於玻璃/模糊表面,在進入/離開時同時對模糊半徑和縮放進行動畫,這樣表面讀起來就像一個真實的材質到達,而不是單純的不透明度淡入。
.toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* 亮色頂邊 = 光線捕捉到材質 */
}
13. 多模態回饋——動畫 + 聲音 + 觸覺
結合感官的三條規則(來自《Designing Audio-Haptic Experiences》):
- 因果關係——必須清楚是什麼導致了回饋。在實際的因果事件上觸發它(例如開關切換、項目到位),並使其特性與動作的物理性匹配。
- 和諧——視覺、聲音和觸覺必須在同一幀觸發。它們之間的延遲會破壞沉浸感。不要讓 CSS 轉場落後於音訊/觸覺(Vibration API)。
- 實用性——只在有意義的地方加入回饋。將觸覺/聲音保留給重要的時刻(成功、錯誤、確認、到位)。過度回饋會訓練使用者忽略所有回饋。
14. 減少動畫與無障礙
減少動畫不代表沒有回饋——它意味著更溫和、不影響前庭系統的等效方式。回應三個獨立的訊號並將其融入你的元件:
prefers-reduced-motion: reduce——將滑動/彈簧/視差效果替換為簡短的不透明度交叉淡入淡出或靜態轉場。移除彈性/overshoot。保留有助於理解的透明度/顏色變化。prefers-reduced-transparency: reduce——讓半透明表面變得更不透明/實色:提高背景不透明度,移除模糊。prefers-contrast: more——使用接近實色的背景,並加上明確的對比邊框。
此外:避免全視口移動背景、緩慢循環的震盪(接近 0.2 Hz / 每 5 秒一個週期),以及突然的亮度跳躍(淺色↔深色主題切換應平滑)。讓大型移動物體在移動時保持半透明,並在大型重新定位時淡出大表面,穩定後再淡入。
@media (prefers-reduced-motion: reduce) {
.sheet { transition: opacity 200ms ease; transform: none !important; }
}
@media (prefers-reduced-transparency: reduce) {
.toolbar { background: white; backdrop-filter: none; }
}
15. 字體排印——光學尺寸、字距、行距
Apple 設計字型時會根據尺寸改變形狀;同樣的原則也適用於網頁。(來自《The Details of UI Typography》,WWDC 2020。)
- 字距(letter-spacing)是尺寸相關的——絕不要對所有尺寸使用同一個值。 大型顯示文字需要負字距(字體變大時字母看起來太分散);小型文字需要稍微正字距以保持可讀性。固定的
letter-spacing在某些地方一定是錯的。標題要緊縮,內文接近0。 - 行距(line-height)與尺寸成反比。 大型標題要緊湊,內文要寬鬆。對於有高上升/下降筆畫的字體要增加行距;對於密集、資訊量大的 UI 要緊縮。
- 將層級建立在字重 + 尺寸 + 行距的組合上,而不只是尺寸。用字重來強調——它能在不佔用更多空間的情況下增加存在感。
- 尊重使用者的文字大小設定(Dynamic Type)。讓佈局隨著文字縮放——使用
rem/em作為間距單位,而不是固定的 px——這樣較大的字體就不會破壞佈局。 - 在自訂字體之前,預設使用平台的系統字體;它已經內建了光學尺寸、字距表和可讀性調整。只有在有理由時才覆蓋。
:root { font: 100%/1.5 system-ui, sans-serif; } /* 內文:系統字體,舒適行距 */
.display {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05; /* 大型文字緊湊行距 */
letter-spacing: -0.02em; /* 隨著尺寸增大使用負字距 */
font-optical-sizing: auto;
}
16. 設計基礎——八項原則
上述的動畫與工藝服務於 Apple 的八項設計原則(《Principles of Great Design》,WWDC 2026)。將這些作為你推理時使用的名稱:
- 目的。 有意識地製作;決定不建什麼。每個功能都在索取使用者的時間、注意力和信任——只在有回報的地方花費這個預算。
- 主導權。 讓使用者保持控制:提供選擇,不要強迫單一路徑。用寬容來支持——輕鬆復原失誤,只有對真正破壞性、不可逆的操作才使用確認對話框(謹慎使用;過度使用會訓練人們直接點過)。
- 責任。 以使用者的利益行事。隱私:在正確的時機詢問,只詢問必要的內容,保持透明。安全:預測誤用和傷害——尤其是 AI(一個過敏食譜應用程式絕不能建議有害的食材)。加入預覽、確認、免責聲明;刪除風險大於價值的功能。
- 熟悉感。 建立在人們已知的事物上。使用既不太具體也不太抽象的隱喻(垃圾桶代表刪除),並尊重它們的物理特性。保持一致:看起來相同的東西必須行為相同且位於相同位置(在 macOS 上關閉永遠在左上角),這樣人們才能預測接下來會發生什麼。只有當你能證明更好時才打破熟悉的模式——然後測試它,不要假設。
- 靈活性。 為不同的情境、裝置和各種能力設計。適應平台(iPhone = 快速觸碰;桌面 = 精確指標控制的深度工作流程)和情境。包容性設計(年齡、語言、專業知識、無障礙)。當沒有單一佈局適合所有人時,讓使用者個人化——重新排列控制項,隱藏他們不用的東西。
- 簡潔——不是極簡。 去除不必要的東西,讓核心目的閃耀;把所有東西藏在一個地方看起來極簡,但並不簡潔。要簡潔(白話文、無術語、步驟少)且清晰(使用層級——順序、間距、對比——讓最重要的東西最明顯)。每個元素都必須證明自己的存在;有時加入上下文反而能簡化(例如顯示剩餘時間的影片進度條)。先顯示常見路徑,進階選項再往下一層。
- 工藝。 不妥協的細節關注建立信任。優美的字體排印、適應淺色/深色的色彩、清晰的圖示,以及提供即時自然回饋的反應式動畫。沒有什麼是隨機的——每個間距、時間和對齊數值都是你可以辯護的刻意選擇。抖動的滾動、未對齊的圖示、以及旋轉時破版的佈局,讀起來就像粗心大意。工藝需要迭代和長壽——隨著功能和硬體變化持續演進設計。
- 喜悅。 這是把其他七項做對的結果,而不是事後貼上的五彩紙屑。決定你希望人們感受到的情緒(平靜、自信、興奮),並在每個決策中強化它。
服務於這些原則的戰術規則:
- 回饋有四種: 狀態、完成、警告、錯誤。確認有意義的動作,揭露進行中的狀態,在問題發生前警告,即時驗證(而不是在提交時)。
- 尋路。 每個畫面都應該回答:我在哪裡?我可以去哪裡?那裡有什麼?我如何離開?絕不讓使用者陷入困境。
- 分組與對應。 接近性暗示關係;將控制項放在它所影響的項目附近,並安排控制項以反映它們所改變的內容。如果你需要用標籤來說明控制項,那麼對應關係就很弱。
- 直接、具體的標籤勝過安全的通用標籤。 根據內容命名導航項目(例如「進度」、「資料庫」),而不是模糊的總稱(例如「首頁」)。具體性創造可預測性。
17. 流程
- 互動式原型——一個互動式示範勝過「一百萬個靜態設計」。 你透過建構和操作來發現介面;一個可運作的原型也設定了具體的標準,防止最終實作品質平庸。
- 同時設計互動與視覺。 「你不應該能分辨出一個在哪裡結束、另一個從哪裡開始。」動畫不是像素完成後才加上的一層。
- 在真實情境中與真實使用者測試,並以全新的眼光審視動畫——用慢動作/逐幀播放來捕捉全速時看不見的東西。
快速參考
| 需求 | 技巧 | 具體數值 |
|---|---|---|
| 預設 UI 彈簧 | 臨界阻尼,無 overshoot | damping 1.0,response 0.3–0.4 |
| 動量/輕彈彈簧 | 欠阻尼,輕微彈跳 | damping ~0.8,response 0.3–0.4 |
| 手勢 → 彈簧速度 | 交接放開速度 | 若正規化則為 gestureVelocity / (target − current) |
| 輕彈落點 | 投射動量 | current + (v/1000)·d/(1−d),d ≈ 0.998 |
| 乾淨中斷 | 從顯示(即時)數值開始 | 讀取螢幕上的 transform |
| 避免反向「磚牆」 | 在重新設定目標時保留速度 | 能混合速度的彈簧 |
| 可逆轉場 | 鏡像 easing 曲線 | 反向 cubic-bézier |
| 決定反向或確認 | 使用速度的正負號,而不是位置 | 在放開時 |
| 1:1 拖曳 | Pointer Events + capture | 尊重抓取偏移 |
| 回饋 | 在手指按下時,持續提供 | 絕不只發生在結束時 |
| 邊界 | 橡皮筋效果,不要硬停止 | 漸進式阻力 |
| 半透明 chrome | backdrop-filter 層 |
內容在其下方滾動 |
| 字體字距 | 尺寸相關,絕不固定 | 大型文字緊縮(-0.02em),內文接近 0 |
| 減少動畫 | 交叉淡入淡出,而不是滑動/彈簧 | @media (prefers-reduced-motion) |






