fluid-interactions について
motionとReactを使い、操作に応じて動くジェスチャー主体のUIを構築するスキルです。アニメーションの割り込み、慣性、端での抵抗、視差効果の低減に関する実装パターンを示します。
主な機能
- 01リリース速度を引き継ぐスプリング操作
- 02割り込み可能なアニメーション
- 03慣性とスナップ位置の指針
- 04方向ロックと端での抵抗
- 05視差効果の低減への対応
- 06Reactフックと物理計算ユーティリティ
活用シーン
- 01ドラッグ可能なシートの実装
- 02スワイプ式ナビゲーションの構築
- 03カスタムスクローラーへの慣性追加
- 04メディアの細かなシーク操作
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rndrealm/ fluid-interactions --skill fluid-interactionsインストール後、Claude Code で/fluid-interactionsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
code/physics.tsuse-drag.tsuse-motion-preference.tsreferences/direction-lock.mddrag-and-momentum.mdgesture-modes.mdmorphing.mdpointer-events.mdreduced-motion.mdscrubbing.mdsheets.mdsprings.mdswipe-back.md詳細
流れるようなインタラクション
動きがタイムラインを再生するのではなく、物理的なオブジェクトのように振る舞うインターフェースです。次のようなものが含まれます。
- ジェスチャー: 指の動きに追従し、指を離したときの速度を引き継ぎ、フリックした方向に着地し、端では抵抗し、アニメーションの途中でつかめる要素。
- トランジション: タップした要素から広がり、ドラッグして戻せるシート、ナビゲーション、ビュー。
- ジェスチャーモード: 動かし方に応じて異なる動作をするジェスチャー。
- あらゆる環境で適切に動かす: タッチ操作の癖、パフォーマンス、モーションの軽減。
motion を使用する
これらのインタラクションは、必ず motion を使って構築してください。スプリングは速度を引き継いで動き始め、モーション値は再レンダリングせずに更新され、視差効果を減らす設定にも対応します。これらは、このスキルが頼りにしている機能です。GSAP、anime.js、CSS トランジションは使わないでください。ここにあるコードと参考資料は motion を前提としています。(カスタムスクローラーで惰性移動させる手書きのループも、モーション値に書き込み、motion のスプリングに引き継ぎます。)
package.json に含まれていない場合は、コードを書く前にプロジェクトのパッケージマネージャーを使ってインストールしてください(ロックファイルを確認します)。
npm install motion # package-lock.json
pnpm add motion # pnpm-lock.yaml
yarn add motion # yarn.lock
bun add motion # bun.lock / bun.lockb
- React:
motion/reactからインポートします(motion、animate、useMotionValue、useTransform、MotionConfig、useReducedMotion)。 - すでに
framer-motionがある場合: これは旧名称の motion です。そのまま使用し、framer-motionからインポートしてください。両方をインストールしないでください。 - すでに別のアニメーションライブラリを使用している場合: 既存の用途にはそのライブラリを引き続き使い、これらのインタラクションには motion を追加してください。こうしたパターンを別のライブラリで作り直さないでください。
継続時間とイージングカーブ(elastic、back、キュービックベジェのオーバーシュート)を使って、スプリングを決して模倣しないでください。これらでは指の速度を引き継いで動き始めることができません。
ルール
- 時間ではなく、スプリングでアニメーションさせます。 スプリングに必要なのは目標値と開始時の速度だけなので、指が離れた位置から動きを引き継げます。スプリングは、生の剛性や減衰ではなく、
springFromResponseを使って応答時間(秒)と減衰比で指定します。スプリングを参照してください。 - 指を離したときの速度をスプリングに渡します。
VelocityTrackerでポインターを追跡し(直近100msのみ)、velocityをアニメーションに渡します。これを渡さないと、要素はいったん1フレーム停止してから動き始めます。 - あらゆる動きを中断可能にします。 ポインターが押されたら、アニメーションを
stop()し、現在値を新しい起点として読み取ります。アニメーション中も入力をブロックしないでください。状態(開いているか閉じているか、現在のページはどれか)は、値やonCompleteではなく、アニメーションが向かっている先から判断します。スプリングは完了前に止まったように見えるためです。ドラッグと慣性と値ではなく目標値で判断するを参照してください。 - 動き出した方向から、着地点を決めます。 リリース時に
project(velocity)を使うと、フリックした場合に惰性で進む位置がわかります。リリース位置ではなく、その位置から最も近い目標値にスナップさせます。これにより、短く速いフリックで途中の停止位置を飛び越えられます。 - 端ではラバーバンド効果を使います。 急に止めるのではなく、
applyRubberBand(value, min, max, d)を使います。引っ張るほど、動きは小さくなります。 - 操作につながらない方向には抵抗を加えます。 一方の方向にしか動作がない場合(下方向で閉じる、右方向で戻るなど)、もう一方の方向でも動くようにしますが、ラバーバンド効果を適用し、リリース時にスプリングで戻します。動きを完全に止めたり、自由に動かせたりしないでください。自由に動くと、存在しない操作があるように見えてしまいます。これは、ユーザーが自由に動かせるように押さえているものに限って適用します。もう一方の軸が別の要素(ページスクロールや親リスト)に属する場合は、ラバーバンド効果を使わず、方向ロックで操作を引き継ぎます。ドラッグと慣性を参照してください。
- 操作する前に方向をロックします。 約10px動くまで待ってから、軸を決定します。ポインターを捕捉するのはロック後にし、タップやもう一方の軸(通常はネイティブスクロール)の操作を妨げないようにします。方向ロックを参照してください。
- フレームごとの処理でReactの状態を変更しないでください。 refやモーション値(またはDOMへの直接書き込み)を使います。状態を設定するのは、リリース時か離散的な変化があったときだけにします。
- レイアウトではなく、トランスフォームで移動させます。
x、y、scale、opacityを使います。レイアウト(getBoundingClientRect、clientWidth)を読み取るのはジェスチャーの開始時か終了時に一度だけとし、毎フレーム読み取らないでください。 - ジェスチャーを必ず終了させます。
pointerup、pointercancel、lostpointercaptureを処理します。終了イベントを取りこぼしたジェスチャーがあると、UIが動かなくなります。ポインターイベントを参照してください。 - 動きを抑える設定には、ユーザーの指で動かすものではなく、自動で動くものを控えめにすることで対応します。 動きを抑える設定を参照してください。
- 1つのジェスチャーに複数の意味を持たせられます。 動き方(開始位置、距離、速度、一時停止、別の軸への移動)によって、関連する操作を選び分けられます。たとえば、上スワイプでホームに戻るか、一時停止してアプリスイッチャーを開くか、スクラバーから離れるように動かして細かくスクラブするかを選べます。基本の操作は常に機能するようにし、リリース前にモードがわかるようにしてください。ジェスチャーモードを参照してください。
コード
以下をプロジェクトにコピーして、必要に応じて調整してください。physics.tsには依存関係がありません。フックにはReactが必要で、use-motion-preference.tsにはmotionが必要です。
- code/physics.ts:
springFromResponse、noBounce、VelocityTracker、rubberBand、applyRubberBand、rubberBandInverse、project、DECELERATION、nearest、nearestPoint、clamp、FADE。 - code/use-drag.ts:開始位置からのオフセットとリリース時の速度を通知し、必ず
onEndを呼び出す、ポインターイベントを使ったドラッグ用フックです。 - code/use-motion-preference.ts:
{ reduceMotion, spring }を提供します。動きを抑える設定が有効な場合、spring()はバウンスをなくします。
すべてのジェスチャーの基本形
const x = useMotionValue(0);
const origin = useRef(0);
const drag = useDrag({
onStart: () => {
x.stop(); // catch it mid-flight
origin.current = x.get(); // continue from where it is on screen
},
onMove: (delta) => {
x.set(applyRubberBand(origin.current + delta.x, min, max, width));
},
onEnd: (velocity) => {
const landing = x.get() + project(velocity.x);
animate(x, nearest(landing, targets), {
...spring(SNAP),
velocity: velocity.x,
});
},
});
return (
<motion.div {...drag} style={{ x }} className="touch-none select-none" />
);
パターン
実装するパターンのリファレンスをお読みください。
| 実装するもの | 読むもの |
|---|---|
| あらゆるスプリング、ピッキングの応答、減衰 | スプリング |
| ドラッグまたはフリック可能な要素、角や点へのスナップ、慣性とページングを備えたカスタムスクローラー | ドラッグと慣性 |
| 縦リスト内の横スワイプ、スワイプアクション、引っ張って更新 | 方向ロック |
| デテント付きボトムシート | シート |
| 細かなスクラブ操作に対応した動画または音声のスクラバー | スクラブ操作 |
| 詳細ビューに広がり、下にドラッグして閉じるサムネイル | モーフィング |
| ナビゲーションスタックを戻るためのエッジスワイプ | スワイプバック |
| 複数の結果やモードを持つ1つのジェスチャー(ホームバー、細かなスクラブ操作、伸長するスワイプアクション) | ジェスチャーモード |
| モーションの軽減 | モーションの軽減 |
| タッチ操作でジェスチャーがフリーズする、固まる、またはタップが機能しなくなる場合 | ポインターイベント |
適切なデフォルト値
| 項目 | 値 |
|---|---|
| ほとんどのUIスプリング | springFromResponse(0.35–0.45, 0.85–0.9) |
| ナビゲーション、ページング、オーバーシュートさせたくないもの | 減衰比 1 |
| 速度の計測ウィンドウ | 直近の100ms |
| 方向ロックのしきい値 | 10px |
| 厳密な軸ロックの比率 | 優勢な軸がもう一方の2倍 |
| ラバーバンド定数 | c = 0.55、d = その軸におけるコンテナーのサイズ |
| 投影減速 | 0.998(通常)、0.99(高速、早く停止) |
| エッジスワイプ領域 | 端から24px |
| モーション軽減時のフェード | 200msのイーズアウト |
2つの選択肢にはそれぞれ指針があるため、推測しないでください。スプリングをどの程度バウンスさせるか(スプリング)と、スクローラーを通常どおり慣性で動かすか、すぐに停止させるか、ページングさせるか(ドラッグと慣性)です。迷った場合は、バウンスなし、通常の減速にしてください。
完了とする前のチェックリスト
- アニメーション中につかみます。ジャンプせず、その場でぴたりと止まります。
- アニメーションが終わったように見えた瞬間にもう一度操作します(別の行を開く、表示されたページをタップする、反対方向にスワイプする)。すぐに反応し、反応のない四分の一秒がありません。
- はじきます。指を離したときに引っかからず、指の速度で動きます。
- 端を越えてドラッグします。抵抗して、ばねのように戻ります。
- 範囲内のどこにでも置けるものをドロップします。揺れずに、置いた場所にぴたりと留まります。
- 何も起こらない方向にドラッグします。反応しなくなることも自由に動くこともなく、抵抗して戻ります。
- スマートフォンでゆっくりドラッグします。指に追従し、ページはスクロールしません(
touch-action)。 - ドラッグ可能な要素内のボタンや行をタップすると、きちんと機能します。
- ドラッグ中にもう一本指を置いても、何も問題が起きません。
- ドラッグ中にアプリを切り替えたり、コンテキストメニューを開いたりすると、所定の位置に落ち着き、動かないままにはなりません。
- 視差効果を減らす設定をオンにすると、オーバーシュートせず、大きな自動移動は徐々に消えます。ドラッグの動きは変わりません。