motion について
motion は、画面全体の UI アニメーション、ジェスチャー、モーションブラーの振る舞いを定めるスキルです。UI の動きを実装する開発者向けに、時間、イージング、動きを減らす設定、実装ルールを詳しく示します。
主な機能
- 01時間とイージング曲線の指定
- 02UI 状態遷移の指針
- 03ジェスチャーと閉じ方の定義
- 04@web-kits/motion-blur によるモーションブラー
- 05動きを減らす設定への対応
活用シーン
- 01ページやパネルの遷移実装
- 02ドラッグやスワイプで閉じる UI の構築
- 03動く要素へのモーションブラー追加
- 04動きを減らす設定へのアニメーション対応確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ raphaelsalaja/ skills --skill motionインストール後、Claude Code で/motionを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlrecipes.md
詳細
モーション
このスキルでは、あらゆる画面での動きを扱います。アニメーションの対象、曲線と時間、ジェスチャー、終了時の動き、モーションブラー、モーションの軽減について説明します。
時間と曲線は厳密です
プロジェクトにモーショントークンがある場合は、それを使います。ない場合は、ここに記載された時間、曲線、スケールを厳守してください。ユーザーが開始するモーションが300msを超えて続く場合、スタイルを問わず指摘対象です。
モーションブラーの描画には@web-kits/motion-blurを使用します。デフォルト値は厳密に定められており、シャッター角度は180°、Echoは1.5pxごとに最大16コピー、ブラーの上限は1.05です。
アニメーション自体が必要かどうかはrestraintで扱います。既存のアニメーションを分解する場合はdeconstructで扱います。フレームレートを維持できるかどうかはframesで扱います。サーフェス、読み込み、ホバースタイル、サウンドはdefaultsで扱います。
アプリのクロームはフェードさせる
ページ切り替え、パネル、読み込み状態にはopacityを使ってフェードを適用し、スライドやスケールは使いません。これはmotion-opacity-for-chromeです。変化する軸だけをアニメーションさせてください。高さだけが変わるのに幅をアニメーションさせるのはmotion-changing-axis-onlyです。
入場アニメーションは再生しません
ページを切り替えても、入場アニメーションは再生しません。ルートレベルのAnimatePresenceと、状態の切り替えをラップするものにはinitial={false}を設定します。これはmotion-no-replay-on-entryです。キャッシュされたビューは、フェードやスケルトンのちらつきなしにすぐ描画されます。これはmotion-no-flash-on-returnです。recipes.mdを参照してください。
アニメーションさせるプロパティを指定します
トランジションでは、通常opacityやtransformなど、アニメーションさせるプロパティを列挙します。transition: allはmotion-name-the-propertiesです。
切り替わる状態にはトランジションを使い、キーフレームは一度だけ実行します
インタラクティブな状態の変化にはトランジションまたはスプリングを使います。ユーザーが逆方向に操作すると、動きの目標が切り替わります。切り替え可能なクラスに設定されたキーフレームアニメーションはmotion-transitions-for-stateです。キーフレームは一度だけ実行されるシーケンスに使ってください。
モーションは発生元から始めます
ドロップダウンやポップオーバーはトリガーからスケールさせ、transform-originをその端に設定します。メニューが自身の中央から広がる場合はmotion-origin-from-triggerです。モーダルにはトリガーとなる基点がないため、中央にとどまります。
ゼロから拡大させません
入場時はopacity: 0とともに、scale(0.95)以上から始めます。scale(0)はmotion-no-scale-from-zeroです。
ジェスチャーにはスプリングを使います
ドラッグ、スワイプ、移動中にユーザーが反転できるあらゆるモーションにはスプリングを使います。指を離すときは、ポインターの速度をスプリングに渡します。固定のease-outを使ったドラッグ終了はmotion-spring-for-gesturesです。recipes.mdを参照してください。
フリックで閉じ、端では抵抗を加えます
スワイプは距離または速度に応じて閉じます。そのため、速度が約0.11 px per millisecondを超えると、短いフリックでも十分です。速いフリックを無視するシートはmotion-flick-dismissesです。
境界を越えてドラッグすると、壁にぶつかるのではなく、摩擦が強まります。上端で急停止するドロワーはmotion-friction-at-edgesです。ドラッグ開始時にポインターをキャプチャし、追加のタッチは無視します。recipes.mdを参照してください。
すべてのジェスチャーにボタン操作を用意します
ドラッグ、スワイプ、ピンチのそれぞれに、クリックとキーボードでも操作できる手段を用意します。ドラッグ操作だけで並べ替えるリストはmotion-gesture-alternativeです。
変化に合ったイージングを使います
トーストなどのシステム上の変化には、スプリングではなくイージングを使います。入場と終了にはイーズアウトを、ビューの切り替えにはイーズインとイーズアウトの両方を使います。リニアはプログレスバーと時計にのみ使います。ease-inで入場するモーダルはmotion-ease-by-directionです。
組み込みのease-outキーワードは弱すぎます。入場と終了にはcubic-bezier(0.23, 1, 0.32, 1)を使い、画面上の移動にはcubic-bezier(0.77, 0, 0.175, 1)を使います。単独のease-outはmotion-strong-ease-outです。
閉じる動作は開く動作より速くします
閉じる動作は開く動作より短くします。閉じるときやホバー解除時に遅延を加えません。表示にかかった時間と同じだけかけて閉じるモーダルはmotion-close-fasterです。
モーションは300ms未満にします
ホバー時の色変化はトランジションなしで瞬時に切り替わります。ホバー時にボタンの背景を200msかけてフェードさせる動作は、motion-instant-hover-colorに該当します。
押下時やホバー時の移動には120~180msかけます。小さな状態変化には180~260msかけます。ユーザーが開始する動作は、パネル全体やドロワーの最大400msを除き、300msを超えないようにします。ドロップダウンに500msかける動作は、motion-under-300msに該当します。
mode="wait"では、退場と登場が順番に再生されるため、それぞれの時間を半分にします。動きが遅く感じる場合は、イージングを変える前に時間を短くします。サンプルはrecipes.mdにあります。
頻繁に行う操作にはアニメーションを付けません
入力、キーボードショートカット、キーボードによるナビゲーション、高速な切り替えにはアニメーションを付けません。キーボードで開いたコマンドパレットは即座に表示します。コンテキストメニューも即座に表示し、閉じるときはフェードアウトさせてもかまいません。キー入力のたびにスケールを変化させる動作は、motion-no-motion-on-repeatに該当します。
ツールチップの待機時間は最初の一度だけにします
グループ内の最初のツールチップは、遅延後に開きます。いずれかのツールチップを表示している間は、その隣のツールチップは即座に、アニメーションなしで開きます。すべてのツールチップは即座に閉じます。ツールバーの各ツールチップが毎回遅延時間いっぱい待つ動作は、motion-tooltip-delayに該当します。
アイコンの切り替えにはクロスフェードを使います
コピーからコピー済みへの切り替えのように、アイコンをその場で切り替える場合はクロスフェードさせます。スケールは0.25から1、不透明度は0から1、ぼかしは4pxから0に変化させます。即座に切り替える動作は、motion-icon-crossfadeに該当します。recipes.mdを参照してください。
iconsのモーフ形式にある2つのアイコンは、代わりにモーフさせます。移動のイージングを使って、それぞれのパスのdを小さな状態変化としてアニメーションさせます。モーション軽減設定が有効な場合は、クロスフェードさせます。モーフ形式の2つのアイコンをクロスフェードさせる動作は、motion-icon-morphに該当します。recipes.mdを参照してください。
クロスフェードにはぼかしを加えます
2つの状態間のクロスフェードが、それでも2つのオブジェクトのように見える場合は、実行中にfilter: blur(2px)を加えます。Safariでは負荷が高くなるため、ぼかしは20px未満にします。切り替えの途中で2つのラベルが目に見えて重なる動作は、motion-blur-the-crossfadeに該当します。
モーションブラーは目で追えない場合にだけ使います
画面自体が目で追うものをすでにぼかしているため、さらにぼかしを加えると、読んでいるテキストが灰色の帯になってしまいます。投げたカード、閉じたシート、スナップして表示されるパネル、スピナーをぼかします。スクロール、カルーセルのページ、所定の位置にスライドするリスト、読んでいるテキストをぼかす動作は、motion-blur-untracked-onlyに該当します。
ReactではuseMotionBlurを、その他の環境ではmotionBlur(element)を使って残像を描画します。このパッケージは速度、フレーム時間、シャッター速度に応じてサイズを調整し、イージングに追従して、停止時に解除します。動く要素に手書きのfilter: blur()やコピーのループを使う動作は、motion-blur-packageに該当します。recipes.mdを参照してください。
ぼかすのは動く要素です
このパッケージは、ホスト自身のトランスフォームを監視します。子要素がアニメーションしているときにラッパーにrefを設定しても何も描画されず、motion-blur-ref-on-moverに該当します。MotionBlurコンポーネントを使う場合は、コンポーネント自体をアニメーションさせ、その内側にある要素にスタイルを設定します。
ホストの最初の子要素がレイヤーとなり、残像はそのレイヤー内に描画されます。背景、境界線、シャドウはレイヤー自体かその内側に設定します。ホストに設定した背景は残像の下で鮮明なままとなり、motion-blur-transparent-hostに該当します。
この手法ではレイヤーのトランスフォームを書き換えるため、レイヤー自体にはトランスフォームを設定しません。動いている間にレイヤーの傾きやスケールが失われる動作は、motion-blur-bare-layerに該当します。内側の要素に設定してください。
残像の描画にはEchoを使います
デフォルトのecho()を使います。カメラのブラーのような形状を持ち、回転、スケール、イージングに追従する唯一の手法です。framesのトレースでコピーのコストが高すぎるとわかった場合にだけ、squeeze()、svg()、smear()を使います。
この3つはレイヤー全体を一定量、一定方向にぼかしますが、回転する要素の縁はそれぞれ異なる速度で動きます。Echo以外を使ったスピナーやオービットは、motion-blur-echo-for-turnsに該当します。テキストを含む要素で、スミアの上限を1.05より大きくする動作は、motion-blur-smear-limitに該当します。
squeeze()とsvg()はレイヤーとその子要素を反対方向に回転させるため、レイヤーが包む要素は1つだけにします。子要素が2つあるレイヤーはエラーになり、motion-blur-one-childに該当します。
曲線にはパスを渡します
物理演算のループなど、手動で設定した位置では、フレームごとに blur.render(previous, current) を1回呼び出します。動きが曲がる場合は、代わりにパスを渡すと、複製が曲線に沿って配置されます。軌道上の2つの変換には motion-blur-follow-path を使います。recipes.mdを参照してください。
デモを通常の4分の1の速度で再生すると、シャッター値を4倍にする必要があります。そのため、shutter: 720 にすると、通常速度と同じ長さの軌跡を保てます。スローモーションの制御を180°のままにしている場合は motion-blur-slowdown-shutter です。
カスタムテクニックはすべてを元に戻します
カスタムテクニックは、clear で paint が設定したすべてのスタイルを削除し、destroy でページに追加したものをすべて削除するペインターを返します。レンダリング内で作成されたテクニックには key も設定されるため、同等のテクニックではペインターが再構築されません。静止した要素にフィルターが残ったままの場合、または key のないインラインテクニックは motion-blur-custom-technique です。recipes.mdを参照してください。
押下時の拡大縮小は控えめにします
押下されたコントロールは 0.98 に拡大縮小し、つぶしや伸ばしは 0.95–1.05 の範囲に収めます。whileTap={{ scale: 0.8 }} は motion-press-scale です。リスト項目のスタガー間隔はそれぞれ50ms未満にします。リストでの staggerChildren: 0.15 は motion-short-stagger です。タイトル、テキスト、アクションなど、1回だけ再生する段階的な導入では、100ms間隔でスタガーします。通常のインタラクションではスタガーを使わず、スタガーによって入力を妨げることもありません。
終了アニメーションにはプレゼンス境界が必要です
exit を持つ条件付き要素は、安定したIDをキーとして AnimatePresence 内に配置し、initial の値へと終了します。ラッパーがないために終了アニメーションが再生されない場合は motion-exit-in-presence です。recipes.mdを参照してください。
コンポーネントライブラリのダイアログはマウントしたままにし、open プロパティを切り替えます。open && の背後でレンダリングされ、閉じるアニメーションが失われるダイアログは motion-dialogs-stay-mounted です。
サイズは2つ目の要素のコンテンツに合わせます
コンテナの幅または高さをアニメーションさせるには、内側の要素を ResizeObserver で計測し、外側の要素をアニメーションさせます。同じノードを計測してアニメーションさせる場合は motion-measure-inner です。recipes.mdを参照してください。
パネルが折りたたまれている間、コンテンツは開いているときの幅を保つため、アニメーション中に再フローは発生しません。サイドバーが閉じるときに、サイドバーのコンテンツに w-full を設定する場合は motion-hold-content-size です。
動きを減らす設定では移動をなくします
prefers-reduced-motion: reduce に一致する場合、スライド、拡大縮小、ぼかしは不透明度のクロスフェードに切り替えます。この設定を無視するスライド式ドロワーは motion-reduced-motion です。
動きのぼかしは、この設定が有効な場合に軌跡をなくし、reducedMotion は "user" のままにします。reducedMotion: "never" は motion-blur-reduced-motion です。
ペア
すべてのチェックはペアになっています。問題のある行の隣に、それを置き換える行が配置されています。
| Rule | Broken | Replacement |
|---|---|---|
motion-opacity-for-chrome | ページ上の initial={{ x: 20, scale: 0.95 }} | initial={{ opacity: 0 }} |
motion-changing-axis-only | 高さだけが変わるときの animate={{ width, height }} | animate={{ height }} |
motion-no-replay-on-entry | ルートを囲む <AnimatePresence> によって、入場アニメーションが再生される | <AnimatePresence initial={false}> |
motion-no-flash-on-return | キャッシュ済みのビューでスケルトンが一瞬表示される | キャッシュ済みのビューをすぐにレンダリングする |
motion-name-the-properties | transition: all 200ms | transition: opacity 200ms, transform 200ms |
motion-transitions-for-state | .open { animation: slide-in 200ms } | 切り替えるプロパティに transition: transform 200ms を指定する |
motion-origin-from-trigger | center を起点に拡大するドロップダウン | 左揃えのトリガーの下に transform-origin: top left を指定する |
motion-no-scale-from-zero | ポップオーバーの initial={{ scale: 0 }} | initial={{ scale: 0.95, opacity: 0 }} |
motion-spring-for-gestures | duration: 0.3, ease: "easeOut" でドラッグを離す | velocity: info.velocity.x を指定した type: "spring" |
motion-flick-dismisses | 固定距離を超えた場合のみ閉じる | 距離、または 1 ミリ秒あたり 0.11 px を超えた場合に閉じる |
motion-friction-at-edges | 上端で急停止するドロワー | ドラッグするほど動きが減衰する |
motion-gesture-alternative | ドラッグでしか並べ替えられない行 | 行メニューに Move Up と Move Down を用意する |
motion-ease-by-direction | ease-in で表示されるモーダル | イーズアウトで表示・非表示にし、終了時は短くする |
motion-strong-ease-out | transition: transform 200ms ease-out | transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1) |
motion-close-faster | 表示も非表示も 200ms のモーダル | 表示は 200ms、非表示は 150ms |
motion-instant-hover-color | ボタンの transition: background-color 200ms | ホバー時の色をすぐに適用する |
motion-under-300ms | ドロップダウンの transition={{ duration: 0.5 }} | duration: 0.2 |
motion-no-motion-on-repeat | キー入力のたびに animate={{ scale: [1, 1.02, 1] }} | 入力中はアニメーションさせない |
motion-tooltip-delay | ツールバーの各ツールチップが毎回、遅延時間いっぱい待つ | 最初のツールチップは待機し、隣のツールチップはすぐに開く |
motion-icon-crossfade | {copied ? <CheckIcon /> : <CopyIcon />} | 両方のアイコンをスケールとぼかしでクロスフェードする |
motion-icon-morph | モーフィングする 2 つのアイコンをクロスフェードする | 各 motion.path の d を対応するアイコンの値へアニメーションする |
motion-blur-the-crossfade | 切り替えの途中でボタンのラベルが重なる | 切り替え中に filter: blur(2px) を適用する |
motion-blur-untracked-only | カルーセルのページをスワイプするときにぼかす | ぼかさない、または勢いよく動かしたカードだけをぼかす |
motion-blur-package | カードが飛ぶ間に filter: blur(8px) を適用する | カードに useMotionBlur() を使う |
motion-blur-ref-on-mover | motion.div を囲むラッパーに ref={blur.ref} を指定する | motion.div に ref={blur.ref} を指定する |
motion-blur-transparent-host | ホストに bg-white rounded-xl を指定する | 同じクラスを最初の子要素に指定する |
motion-blur-bare-layer | レイヤーに rotate-3 を指定する | レイヤー内の要素に rotate-3 を指定する |
motion-blur-echo-for-turns | スピナーに technique: squeeze() を使う | technique: echo() |
motion-blur-smear-limit | テキストを含むカードに smear({ limit: 1.3 }) を使う | デフォルトの 1.05 を使う smear()、または echo() |
motion-blur-one-child | アイコンとラベルを含むレイヤーに squeeze() を使う | アイコンとラベルを 1 つの要素で囲む |
motion-blur-follow-path | 軌道に blur.render(previous, current) を使う | blur.render((t) => at(angle + t * speed * frameTime)) |
motion-blur-slowdown-shutter | shutter: 180 の ¼× デモ | shutter: 720 |
motion-blur-custom-technique | clear() {}、またはレンダー内の technique: ({ layer }) => … | paint が設定したすべてのスタイルを削除する clear() と key |
motion-press-scale | whileTap={{ scale: 0.8 }} | whileTap={{ scale: 0.98 }} |
motion-short-stagger | staggerChildren: 0.15 | staggerChildren: 0.03 |
motion-exit-in-presence | {open && <motion.div exit={{ opacity: 0 }} />} | 同じ要素を <AnimatePresence> の中に置く |
motion-dialogs-stay-mounted | {open && <Dialog.Root open>} | <Dialog.Root open={open} onOpenChange={setOpen}> |
motion-measure-inner | 1 つのノードに ref と animate={{ height: bounds.height }} を指定する | 外側のノードをアニメーションさせ、内側のノードに ref を設定する |
motion-hold-content-size | 折りたたまれるサイドバーの中に w-full のコンテンツを置く | 幅を維持する w-64 のコンテンツを置く |
motion-reduced-motion | モーション低減設定時にもスライドするドロワー | 不透明度のクロスフェード |
motion-blur-reduced-motion | reducedMotion: "never" | デフォルトの "user" |
報告
重大度。 HIGH は使用またはアクセスを妨げます。最後まで再生されない終了アニメーション、自己計測するコンテナ、ボタンのないジェスチャーなどです。また、読み取り中のテキストにかかるモーションブラーや、squeeze() または svg() の下に子要素が2つあるレイヤーも該当します。
MEDIUM はスクロールするページ、再生される入場アニメーション、固定カーブ上のジェスチャー、または300msを超えるモーションです。また、入力中のモーション、誤ったカーブ、transition: all、切り替え状態に対するキーフレーム、または軽減モーションの無視も該当します。また、条件付きでレンダリングされるダイアログ、折りたたみ中に再配置されるコンテンツ、scale(0) の入場アニメーション、またはフリックを無視するシートも該当します。また、手書きのぼかし、ラッパー上のぼかし参照、不透明なホスト、またはレイヤー上のトランスフォームも該当します。また、Echo以外に表示されるスピナー、1.05を超えるにじみ、または reducedMotion: "never" も該当します。
LOW は色が薄くなるホバー色、強すぎる押下時のスケール、または長いスタガーです。また、誤った原点からのモーション、開くときと同じくらい遅い閉じる動作、遅いツールチップ、または唐突なアイコンの切り替えも該当します。また、単独の ease-out、ぼかしのないクロスフェード、または端の摩擦がないドロワーも該当します。また、カーブ上の2つのトランスフォーム、全速シャッターで減速したデモ、またはスタイルを残したり再構築したりするカスタム手法も該当します。
形式。 根本原因ごとに1件の指摘を記載します。
file:line - [rule-id] what is wrong
続けて Rule | Count | Severity の概要を記載します。HIGH が1件でも残っている場合は Block、それ以外は Approve とします。読んでいないファイルを Approve にしてはいけません。タイミングに依存するチェックではモーションを再生し、残像に依存するチェックでは最速の時点で停止します。実行できなかったチェックはすべて Not verified と記します。