motion について
インターフェースの動きを選ぶためのデザインスキルで、時間、プロパティ、イージング、アクセシビリティ、パフォーマンスを解説します。アニメーションの必要性を判断し、CSS、Motion、GSAP で実装するプロダクト UI の制作者向けです。
主な機能
- 01表示頻度に応じた動きの指針
- 02アニメーションの目的と時間のルール
- 03CSS、Motion、GSAP の選び方
- 04プロパティとイージングの推奨
- 05動きを減らす設定への対応指針
- 06実装レシピとアンチパターン
活用シーン
- 01メニューやポップオーバーの動きの選定
- 02ダイアログやトーストのトランジション設定
- 03レイアウト変更やタブインジケーターのアニメーション
- 04動きを減らす設定に応じた UI アニメーション
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ paragonhq/ stealth-skills --skill motionインストール後、Claude Code で/motionを入力して読み込まれたか確認します。
詳細
motion
Motionは、インターフェース制作で最も目につくため、最も乱用されているツールです。このスキルの目的は、アニメーションを減らし、残すものを適切にすることです。
1行書く前に
次の質問に順番に答えてください。どの段階でもコードを一切書かずにタスクを終えて構いません。それが望ましい結果の場合もあります。
1. どのくらいの頻度で目にしますか?
| 表示頻度 | ルール |
|---|---|
| 数秒ごと(入力、リストの移動、行にホバー) | 何もしない、または120ms未満で、ほとんど気づかれない程度 |
| 1日に何度も(メニュー、パレット、タブ、トグル) | 素早いフィードバックのみ、120〜200ms、楽しさを加えない |
| 1日に数回(ダイアログ、ドロワー、シート、トースト) | 入場と退場をしっかり表現、180〜320ms |
| まれ(オンボーディング、成功、初回起動) | 長く表現豊かなアニメーションを使える唯一の場面 |
キーボードで開始するアクションは即時に反映します。 ⌘K、Escape、j/k、ショートカットによるタブ切り替えでは、新しい状態を同じフレームで描画します。ここでのアニメーションは遅延に感じられます。
2. 何のためですか?
1つ挙げてください。挙げられないなら、そこで止めてください。
- 応答 — 押下、切り替え、ドロップへの応答です。インターフェースが操作を認識したことを示します。
- 位置を示す — 要素がどこから来たか、どこへ移動したかを示します。ポップオーバーはトリガーから展開し、トーストは現れたときと同じ方向に消えます。
- つなぐ — 2つのレイアウトをつなぎ、コンテンツが突然移動したように見せないようにします。並べ替え、展開、タブインジケーターなどが該当します。
- 知らせる — 見ていない間に状態が変わったことを示します。保存済みバッジ、カウンターの変化などが該当します。
- 説明する — 仕組みを見せます。マーケティングとオンボーディングに限ります。
「生き生きと見える」は目的ではありません。「ほかのすべてにアニメーションがある」も目的ではありません。
3. どのツールを使いますか?
目的を果たせる最も簡単な方法を、次の順に選びます。
| 必要なもの | 使用するもの |
|---|---|
| ホバー、フォーカス、押下、色、単純な表示 | CSS transition |
| ループまたは複数ステップで、中断が不要 | CSS @keyframes |
| マウントされたReact要素の入場と退場、レイアウト変更、スプリング、中断 | Motion(motion/react) |
| タイムライン、スクラブ、ピン留め、テキスト分割、シーンの連続表示 | GSAP |
| スクロール連動 | まずCSSのスクロール駆動アニメーションを使い、対応できない場合はScrollTrigger |
フェードのためにライブラリをインストールしないでください。プロジェクトにMotionのAnimatePresenceがあるのに、setTimeoutで退場アニメーションを独自実装しないでください。
4. どのプロパティを使いますか?
transformとopacityです。合成処理されるのはこの2つだけで、ほかのプロパティは確実とは言えません。
| 目的 | 使うもの | 使わないもの |
|---|---|---|
| 表示する | opacity 0→1 + scale .96→1またはtranslateY 6px→0 | scale(0)、displayの切り替えのみ |
| コンテンツに合わせて展開する | overflow: hiddenを設定してheightを計測・アニメーションする、またはgrid-template-rows: 0fr→1fr | max-height: 9999pxのアニメーション |
| 端に沿って表示する | clip-path: inset(...) | width |
| ぼかしながら表示する | filter: blur(6px→0)をopacityと組み合わせ、短時間で使う | ぼかしのみ、大きな領域へのぼかし |
| 位置を移動する | レイアウトアニメーションでtransformを使う(Motionのlayout、GSAPのFLIP) | top/leftのアニメーション |
| 色を変える | transitionを使ったbackground-color、color、border-color | filter: hue-rotate |
transition: allは絶対に使わないでください。プロパティを列挙します。そうすることで速くなり、予期しない動作を防げます。
5. どのイージングを使いますか?
| 状況 | イージング |
|---|---|
| 画面に入る | 強いイーズアウト:cubic-bezier(0.16, 1, 0.3, 1) |
| 画面から出る | ここではイーズインを使っても構いません。または同じイーズアウトを再生時間の70%で使います |
| 画面内で移動する(並べ替え、タブインジケーター、サイズ変更) | イーズインアウト:cubic-bezier(0.76, 0, 0.24, 1) |
| ホバーと色 | easeまたはソフトなイーズアウト。expoは使いません |
| 連続動作(マーキー、スピナー、進捗表示) | linear |
| ジェスチャーやポインターに追従する | カーブではなくスプリング |
| iOSスタイルのシートやドロワー | cubic-bezier(0.32, 0.72, 0, 1) |
組み込みのease-in-outとease-outは、意図的に調整したように感じさせるには弱すぎます。上記のカスタムカーブを使い、トークンとして保存してください:--ease-out-expo、--ease-in-out-quart。
入場時にイーズインを使わないでください。ユーザーが最も注目するのは最初の50msです。イーズインはその時間を無駄にします。
6. どのくらいの長さにしますか?
| 要素 | 時間 |
|---|---|
| 押下時のフィードバック | 100–140ms |
| ホバー状態 | 120–180ms |
| ツールチップ、小さなポップオーバー | 140–200ms |
| メニュー、ドロップダウン、セレクト | 160–220ms |
| タブインジケーター、トグルのつまみ | 180–260ms |
| ダイアログ、シート、ドロワー | 220–320ms |
| トーストの表示 | 240–320ms、非表示は180ms |
| ページまたはビューの遷移 | 300–450ms |
| マーケティング向けの表示演出 | 600–1200ms、1回のみ |
退場時の時間は、入場時の約70%にします。100ms未満では見えず、プロダクトUIで400msを超えると待たされているように感じます。迷ったら、20%短くしてください。
7. スプリング
ユーザーがドラッグしたり、投げたりする要素や、物理的な感触が必要な要素には、次を使います。
| 感触 | モーション設定 |
|---|---|
| キビキビしたUI(インジケーター、チップ、小さな動き) | { type: "spring", stiffness: 520, damping: 38, mass: 0.7 } |
| ソフト(カード、パネル、リストの並べ替え) | { type: "spring", stiffness: 260, damping: 28, mass: 0.9 } |
| ポインターに追従(滑らかに動くホバーハイライト) | { type: "spring", stiffness: 650, damping: 45, mass: 0.5 } |
| リリース後にシートを静止させる | { type: "spring", stiffness: 300, damping: 30 } または bounce: 0 と duration: 0.4 |
プロダクトUIでバウンスが適切なことはほとんどありません。スプリングのオーバーシュートは、せいぜい1〜2ピクセルに抑えます。揺れる場合は、減衰を大きくしてください。
8. どこから表示しますか?
transform-origin はトリガーに設定します。右上のボタンから開くポップオーバーは、右上の角から広がります。ダイアログは例外で、中央に配置し、中央を基点に拡大します。トーストは、消えるときに向かう側の端から表示します。
9. 時差表示
一度だけ、同時に表示されるリストに限って時差をつけます。項目間は12〜25msとし、上限を設けます。8項目目以降は遅延を追加しません。そうしないと、ユーザーが読み始めた後に残りの項目が表示されます。再レンダリング時やフィルター変更時には、時差をつけないでください。
10. 動きの中断
メニューが開いている途中でユーザーが閉じた場合、開ききってから閉じるのではなく、その時点から反転して閉じる必要があります。CSSトランジションでは、これがデフォルトで行われます。キーフレームでは行われません。Motionでは行われます。中断可能な動きにキーフレームを使っているなら、そのツールの選択は誤りです。
11. 退場
退場時の動きは意図的に、短く、シンプルにし、多くの場合は不透明度の変化だけにします。ダイアログは、表示時にフェードと拡大を行っても、退場時はフェードだけでかまいません。ユーザーが閉じた後、消え始めるまでの1フレームであっても表示が残ってはいけません。クリック後の最初のフレームから、すでに変化している必要があります。
12. モーションの低減
同じ変更で対応を組み込みます。
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
Motionでは、useReducedMotion() を使い、トランスフォームを不透明度に置き換えます。スムーズスクロールのライブラリは完全に無効にします。アニメーションで表示されるはずだった要素は、すべてそのまま表示します。
レシピ
メニュー/ポップオーバー
.menu {
transform-origin: var(--origin, top left);
transition: opacity 160ms var(--ease-out-expo), transform 160ms var(--ease-out-expo);
}
.menu[data-state="closed"] { opacity: 0; transform: scale(0.96) translateY(-2px); }
.menu[data-state="open"] { opacity: 1; transform: none; }
ダイアログ
<motion.div
initial={{ opacity: 0, scale: 0.97, y: 6 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}
transition={{ duration: 0.26, ease: [0.16, 1, 0.3, 1] }}
/>
背景は別にフェードさせます。表示時は少し長めに、退場時は短めにします。
トースト
配置されている側の端から、イーズアウトで約280msかけて表示します。古いトーストは scale(0.95) と translateY(-8px) で縮小して暗くし、新しいトーストの背後に重ねます。同じ側の端に向けて、約180msで退場させます。ホバー中とタブが非表示の間は、タイマーを一時停止します。
コンテンツに合わせた高さ
.grid { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 240ms var(--ease-in-out-quart); }
.grid[data-open] { grid-template-rows: 1fr; }
.grid > * { overflow: hidden; min-height: 0; }
タブインジケーター
layoutId を持つ1つの要素(Motion)をタブ間でスプリング移動させます。各タブの left/width はアニメーションさせないでください。
行間を滑らかに移動するホバーハイライト
絶対配置したピルを1つ使い、pointerenter で位置を更新し、「ポインターに追従」するスプリングでアニメーションさせます。pointerleave では、150msのフェードで取り除きます。
数値の変化
数値全体をフェードさせるのではなく、数字が回転するようにアニメーションさせます。自分で実装するのではなく、数字の列に対応したライブラリ(NumberFlow)を使います。
テキストの表示(マーケティング向けのみ)
単語に分割し、初回表示時に1回だけ、各単語を opacity 0→1、y 6→0、filter blur(6px→0) でアニメーションさせます。単語間には30msの間隔を設けます。
アンチパターン
| 生成された UI で見られる例 | 代わりに使うもの |
|---|---|
transition: all 0.3s ease | プロパティを個別に指定し、160~220ms のカスタム ease-out を使います |
アイコンに animate-bounce を使う | 何もしません |
すべてのカードがホバー時に影を付けて -4px 浮き上がる | 次のいずれか 1 つにします:境界線を明るくする、背景を 1 段階明るくする |
| 1.5s のスケルトンシマー | 静的なスケルトン、またはゆっくりした 2.5s のパルス |
| ページ読み込み時にすべての要素を表示 | 最大でもタイトルと 1 つの領域だけを、一度表示します |
| あらゆる場面でスピナーを使う | コンテンツにはスケルトン、アクションにのみスピナーを使います |
ドロップダウンに duration: 0.5 を使う | 0.18 |
| プロダクト UI での視差効果 | 使いません |
パフォーマンス
will-change: transform は、まもなくアニメーションする要素にのみ適用し、アニメーション後に削除します。backdrop-filter を使う要素のアニメーションは避けてください。大きなぼかしは負荷が高いため、半径は 8px 未満にし、適用範囲を小さく保ちます。CPU にスロットリングをかけてテストし、フレーム落ちが発生する場合は、時間ではなくプロパティを削ってください。
カタログから
- You Don't Need Animations — 頻度に関する考え方です。
- Motion — これらのレシピで想定している React アニメーションライブラリです。
- GSAP — タイムライン、スクラブ、SplitText。
- Easing Wizard、Anime.js Easing Editor — カーブの作成と比較に使えます。
- Transitions.dev — Web アプリに必要な少数のトランジションです。
- NumberFlow — 適切に実装された数字のロール表示です。
- 60fps、Design Spells — 独自に考案する前に、記録された例を確認してください。
- stealth.pm/build と stealth.pm/tools にもあります。