Apple Design
初期応答
このスキルが最初に特定の質問なしで呼び出された場合、次のようにのみ応答します。
AppleのWWDCデザイン講演から得た知識をWeb向けに翻訳し、流動的なAppleスタイルのインターフェースをWeb上で構築するお手伝いをする準備ができています。
ユーザーが質問するまで、これ以外の情報は提供しないでください。
Appleがどのようにインターフェースを構築し、コンピュータのように感じられるのをやめ、あなたの延長のように感じられるようにするか。この知識は、AppleのWWDCデザイン講演、主に Designing Fluid Interfaces(WWDC 2018)から得たものを、Webプラットフォーム(CSS、Pointer Events、requestAnimationFrame、Motion/Framer Motionのようなスプリングライブラリ)向けに蒸留し翻訳したものです。
一貫したテーマ:アニメーションは画面上の現在値から開始し、ユーザーの速度を継承し、モメンタムを前方に投影し、任意の瞬間に掴んで反転できるときに、インターフェースは生き生きと感じられます。 スプリングは、本質的に割り込み可能で速度を認識するため、これらすべてを自然にするツールです。
中核となるアイデア
「インターフェースを私たちの考え方や動き方に合わせると、魔法のようなことが起こります。それはコンピュータのように感じられるのをやめ、私たちのシームレスな延長のように感じられ始めるのです。」
インターフェースは、物理世界のように振る舞うときに流動的になります。つまり、物事は瞬時に応答し、連続的に動き、モメンタムを運び、境界で抵抗し、動きの途中で方向転換できます。以下のすべては、それに近づくための方法です。
Appleはデザインを4つの人間のニーズに奉仕するものとして捉えています:安全性/予測可能性、理解、達成感、そして喜び。 ここにあるすべてのルールは、そのいずれかに奉仕します。
1. 応答 — レイテンシを排除する
遅延が現れた瞬間、直接性の感覚は「崖から落ちます」。応答は、他のすべてが構築される基盤です。
- リリースではなく、ポインタダウンで応答する。 ボタンは押された瞬間にハイライトします。
click/タッチアップまで待ってフィードバックを表示すると、死んでいるように感じられます。 - あらゆるレイテンシに警戒する。 デバウンス、人工的なタイマー、トランジション待機、および約300msのタップ遅延を監査します。入力パス上にあって不可欠でないものはすべて後退です。
- フィードバックは、インタラクションの間継続的でなければならず、最後だけではありません。 ドラッグ、スライダー、またはドロワーの場合、ジェスチャが完了したときだけでなく、ポインタが動いている間ずっと1:1でUIを更新します。
/* フィードバックは押下時に生きており、瞬間的です */
.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トランジションと
@keyframesを避けてください — それらは途中でスムーズに掴んで反転することができません。スプリングはデフォルトで現在の値からアニメーションするため、割り込みに必要なものに正確に適合します。 - ジェスチャが反転するとき、速度をブレンドしてください — ハードカットしないでください。 反転時にアニメーションを別のものに置き換えると、速度の不連続性、「レンガの壁」が発生します。再ターゲット時に速度を継続するスプリングライブラリはこれを回避します。(これはiOSの加算アニメーションがネイティブに行うことです。Webでは、現在の速度から再ターゲットするスプリングライブラリを選んでください。)
- 2Dモーションを独立したXとYのスプリングに分解します。 2D距離に対する単一のスプリングは、XとYの速度が異なる場合に非同期になります。
4. 動作をアニメーションよりも — スプリングを使う
「アニメーションを、インターフェースによって規定されたものではなく、あなたとオブジェクトの間の会話として考えてください。」
事前にスクリプト化された固定長のアニメーションは新しい入力に応答できません。スプリングはできます — 新しい入力は単にターゲットを変更し、モーションは連続的に保たれます。ユーザーが触れる可能性のあるものにはスプリングを利用してください。
Appleは意図的に物理の3つのパラメータ(質量/剛性/減衰)を、デザイナーにとってより親しみやすい2つのパラメータに置き換えました。これらで考えてください:
- ダンピング比 — オーバーシュートを制御します。
1.0= 臨界減衰、バウンスなし、スムーズな整定。< 1.0= オーバーシュートして振動します。低いほどバウンスが大きくなります。 - 応答時間 — 値がターゲットにどれだけ速く到達するか(秒単位)。低いほどスナッピーです。これは「継続時間」ではありません — スプリングに固定の継続時間はありません。その整定時間はパラメータから生まれます。
デフォルト値:
- ほとんどのUIはダンピング
1.0(臨界減衰)から始めてください — 優雅で気が散りません。 - ジェスチャ自体がモメンタムを運んだ場合にのみ(フリック、スロー、ドラッグリリース)、バウンス(ダンピング ~
0.8)を追加します。単にフェードインしたメニューでのオーバーシュートは間違って感じられますが、フリックしたカードでのオーバーシュートは正しく感じられます。
Appleが出荷している具体的な値:
| インタラクション | ダンピング | 応答時間 |
|---|---|---|
| 移動 / 再配置(例:PiP) | 1.0 | 0.4 |
| 回転 | 0.8 | 0.4 |
| ドロワー / シート | 0.8 | 0.3 |
Webでの対応(Motion / Framer Motion): bounce + duration スプリングAPIは、Appleのダンピング + 応答時間に近いものです。安全なハウススタイルは、デフォルトで damping: 1.0 のスプリングをあらゆる場所に使用し、モメンタム駆動の物理的インタラクションのためにバウンスを予約することです。
import { animate } from 'motion';
// 臨界減衰デフォルト(オーバーシュートなし)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// モメンタムインタラクション — 少しバウンス、ただしフリックが先行した場合のみ
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. Velocity Handoff — ドラッグとアニメーションの継ぎ目
ジェスチャが終了したとき、アニメーションは指の正確な速度で継続しなければならず、ドラッグとアニメーションの間に目に見える継ぎ目があってはなりません。これが「流動的」と「まあまあ」を最も分ける詳細です。
ポインタのリリース速度をスプリングの初速度として渡します。一部のスプリング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をトリガーに設定して、ボタンとコンテンツの間の空間的関係を明確にします。(これは、ポップオーバーがその中央ではなくトリガーからスケーリングするという、同じ起点認識のポイントです。) - 可逆的なトランジションではイージングをミラーリングします、これにより出力パスが復帰パスと一致します(2つの方向に対して逆の3次ベジェ制御点を使用します)。
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はWebのディスプレイ同期クロックです(AppleはCADisplayLinkを使用)。アニメーションはコンポジターに優しいプロパティ —transformとopacity— のみにし、モーションが差し迫っている場所ではwill-changeでヒントを与えます。
12. マテリアルと奥行き — 半透明性が階層を伝える
Appleは半透明マテリアルを、焦点を奪うことなく構造をもたらす浮遊する機能的なレイヤーとして使用します。Webでは、backdrop-filter で近似します。
- ナビゲーション/ツールバー/シートを半透明レイヤーとして構築します(
backdrop-filter: blur()+ 半透明背景)。コンテンツはその下をスクロールします — 固定ストリップを消費する不透明なバーではありません。 - マテリアルの重みが階層をエンコードします: より暗く/より重いマテリアルは構造領域(サイドバー)を分離します。より明るいマテリアルはインタラクティブ要素(ボタン)に注意を引き付けます。明るい半透明サーフェスを別の上に重ねないでください — 可読性が崩壊します。
- より大きなサーフェスはより厚く読み取られるべきです: 小さなチップよりも強いぼかしと深い影。状況に応じた影を検討してください — 忙しい/テキストコンテンツの上ではより重くして分離を、プレーンな背景の上ではより軽くします。
- ディミングで焦点を合わせ、分離で流れを保ちます。 モーダルタスクはサーフェスと暗くするスクリムをペアにし、背景を押し戻します。並行する非ブロッキングパネルは、流れが途切れないようにスクリムなしで半透明性とオフセットを使用します。積み重ねられたシートの場合、各親レイヤーを徐々に暗くし押し戻します。
- バイブランシーは変化する背景の上でテキストの可読性を保ちます。 ぼかした/半透明のサーフェスの上では、フラットなグレーのテキストを使用しないでください — より高いコントラスト、わずかに太いウェイト、および小さな文字間隔の増加を使用します。色は半透明の前景ではなく、ソリッドなレイヤーに配置します。
- ハードな区切りではなく、スクロールエッジ効果。 スティッキーヘッダーの下に1pxの境界線を引く代わりに、浮遊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. マルチモーダルフィードバック — モーション + サウンド + ハプティクス
感覚を組み合わせるための3つのルール(Designing Audio-Haptic Experiences から):
- 因果関係 — フィードバックの原因が何かは明らかでなければなりません。実際の因果イベント(トグルが切り替わる、アイテムが所定の位置にはまる)でトリガーし、その特性をアクションの物理性に合わせます。
- 調和 — ビジュアル、サウンド、ハプティクスは同じフレームで発火しなければなりません。それらの間のレイテンシは錯覚を破壊します。許可しないでください。
- 抑制 — フィードバックがないこともフィードバックです。重要でないアクション(スクロールの終わりに達した、ボタンの無効状態)は沈黙しているべきです。強制しないでください。
…
[翻訳のため省略]