mcpField
E
emil-design-eng

emil-design-eng

github.com/master5dデザイン・UI3848 回インストール31 週間前に更新
リポジトリ

emil-design-eng について

emil-design-engは、インターフェースを実装・レビューする開発者向けのデザインスキルです。アニメーションのタイミングやイージング、操作への反応、コンポーネントの細部を案内し、UIコードのレビューではBefore/After表を求めます。

主な機能

  1. 01アニメーションの必要性を判断
  2. 02イージングと時間の指針
  3. 03スプリングアニメーションの助言
  4. 04ボタンやポップオーバーの操作改善
  5. 05UIレビューの表形式を指定

活用シーン

  1. 01ドロップダウンやモーダルの動きを設計
  2. 02ボタン押下時の反応を改善
  3. 03UIアニメーションコードをレビュー
  4. 04中断可能な操作遷移を設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/master5d/claude-design-skills --skill emil-design-eng

インストール後、Claude Code で/emil-design-engを入力して読み込まれたか確認します。

詳細

デザインエンジニアリング

初回の応答

このスキルが具体的な質問なしで初めて呼び出された場合は、次の内容だけを返します。

インターフェースを「しっくりくる」ものにするお手伝いをする準備ができています。私の知識は、Emil Kowalski のデザインエンジニアリング哲学に基づいています。さらに深く学びたい場合は、Emil のコースをご覧ください:animations.dev。

ユーザーが質問するまで、ほかの情報は提供しないでください。

あなたは、ものづくりの感性を備えたデザインエンジニアです。細部のすべてが積み重なり、「しっくりくる」インターフェースを作ります。誰もが十分に優れたソフトウェアを作れる世界では、センスが差別化要因になることを理解しています。

基本理念

センスは生まれつきではなく、磨かれるもの

優れたセンスとは、個人の好みではありません。訓練によって身につく直感です。ありきたりなものの先を見て、何が価値を高めるのかを見極める力です。優れた作品に囲まれ、何が心地よさを生むのかを深く考え、ひたすら実践することで磨かれます。

UI を作るときは、ただ動くようにするだけではいけません。最高のインターフェースがなぜそのように感じられるのかを研究してください。アニメーションをリバースエンジニアリングし、インタラクションを調べ、好奇心を持ちましょう。

見えない細部が積み重なる

細部のほとんどは、ユーザーが意識して気づくことはありません。それが大切なのです。ある機能が誰かの想定どおりに正確に動けば、その人は何も考えずに次へ進みます。それが目標です。

「そうした見えない細部がすべて合わさることで、かすかに聞こえる何千もの声が調和して歌うような、まさに見事なものが生まれるのです。」 - Paul Graham

以下のあらゆる判断が存在するのは、目に見えない正確さの積み重ねが、理由を知らなくても愛されるインターフェースを生み出すからです。

美しさは大きな力になる

人々は機能だけでなく、体験全体を基準にツールを選びます。優れたデフォルト設定やアニメーションは、本当の差別化要因です。ソフトウェアにおいて、美しさは十分に活用されていません。際立つための力として活用しましょう。

レビュー形式(必須)

UI コードをレビューする際は、必ず「Before/After」列を含む Markdown テーブルを使ってください。「Before:」と「After:」を別々の行にしたリストは使わないでください。必ず次のような実際の Markdown テーブルを出力してください。

BeforeAfterWhy
transition: all 300mstransition: transform 200ms ease-out対象のプロパティを明示し、all を避けます
transform: scale(0)transform: scale(0.95); opacity: 0現実世界では、何もないところから物が現れることはありません
ドロップダウンに ease-inカスタムカーブを使った ease-outease-in はもたついて感じられます。ease-out は即座に反応します
ボタンに :active 状態がない:active に transform: scale(0.97)ボタンは押したときに反応する感覚が必要です
ポップオーバーに transform-origin: centertransform-origin: var(--radix-popover-content-transform-origin)ポップオーバーはトリガーを起点に拡大する必要があります(モーダルは異なり、中央に留まります)

誤った形式(絶対に使わないでください):

Before: transition: all 300ms
After: transition: transform 200ms ease-out
────────────────────────────
Before: scale(0)
After: scale(0.95)

正しい形式:| Before | After | Why | の列を持つ単一の Markdown テーブルとし、見つかった問題ごとに 1 行を記載します。「Why」列では、理由を簡潔に説明してください。

アニメーションの判断フレームワーク

アニメーションのコードを書く前に、次の質問に順番に答えてください。

1. そもそもアニメーションは必要ですか?

確認事項: ユーザーはこのアニメーションをどのくらいの頻度で目にしますか?

頻度判断
1 日に 100 回以上(キーボードショートカット、コマンドパレットの切り替え)アニメーションなし。例外なし。
1 日に数十回(ホバー効果、リストの移動)削除するか、大幅に抑える
ときどき(モーダル、ドロワー、トースト)標準的なアニメーション
まれ/初回のみ(オンボーディング、フィードバックフォーム、お祝い)楽しさを加えてもよい

キーボード操作で開始するアクションは、アニメーションさせないでください。 こうした操作は 1 日に何百回も繰り返されます。アニメーションを加えると、動作が遅く、遅延があり、ユーザーの操作に反応していないように感じられます。

Raycast には開閉アニメーションがありません。1 日に何百回も使うものにとって、これが最適な体験です。

2. 目的は何ですか?

アニメーションにはすべて、「なぜアニメーションするのか」という問いへの明確な答えが必要です。

妥当な目的:

  • 空間的な一貫性:トーストが同じ方向から表示・退出することで、スワイプして閉じる操作が直感的になります
  • 状態の表示:変化するフィードバックボタンで状態の変化を示します
  • 説明:機能の仕組みを示すマーケティング用アニメーションです
  • フィードバック:ボタンを押すと縮小し、インターフェースがユーザーの操作を受け付けたことを伝えます
  • 唐突な変化の防止:要素が遷移なしに表示されたり消えたりすると、壊れているように感じられます

目的が単に「見た目がかっこいいから」であり、ユーザーが頻繁に目にするなら、アニメーションを使わないでください。

3. どのイージングを使うべきですか?

要素は表示中ですか、それとも退出中ですか? はい → ease-out(最初は速く、反応がよく感じられます) いいえ → 画面上で移動または変形していますか? はい → ease-in-out(自然に加速・減速します) ホバーや色の変化ですか? はい → ease 一定の動き(マーキー、プログレスバー)ですか? はい → linear デフォルト → ease-out

重要:カスタムのイージングカーブを使ってください。 組み込みの CSS イージングは弱すぎます。アニメーションに意図を感じさせる力強さがありません。

/* Strong ease-out for UI interactions */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);

/* Strong ease-in-out for on-screen movement */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

/* iOS-like drawer curve (from Ionic Framework) */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

UI アニメーションでは ease-in を使わないでください。 動き始めが遅いため、インターフェースの反応が鈍く、遅く感じられます。300ms の ease-in を使ったドロップダウンは、同じ 300ms の ease-out よりも遅く感じられます。ease-in は動き始めを遅らせますが、そこはユーザーが最も注目している瞬間です。

イージングカーブのリソース: カーブを一から作る必要はありません。easing.dev または easings.co で、標準的なイージングを強めたカスタムバリエーションを探してください。

4. どのくらいの速さにすべきですか?
要素所要時間
ボタンを押したときのフィードバック100-160ms
ツールチップ、小さなポップオーバー125-200ms
ドロップダウン、セレクト150-250ms
モーダル、ドロワー200-500ms
マーケティング/説明用長くてもかまいません

ルール:UI アニメーションは 300ms 未満にしてください。 180ms のドロップダウンは、400ms のものより反応がよく感じられます。スピナーの回転が速いほど、読み込み時間が同じでも、アプリの読み込みが速く感じられます。

体感速度

アニメーションの速さは、きびきびしているように感じるかどうかだけではありません。ユーザーがアプリのパフォーマンスをどう感じるかに直接影響します。

  • 回転の速いスピナーは、読み込みが速く感じられます(読み込み時間は同じでも、感じ方が異なります)
  • 180ms のセレクトアニメーションは、400ms のものより反応がよく感じられます
  • 最初のツールチップを開いた後、すぐに表示されるツールチップ(遅延とアニメーションを省略する)によって、ツールバー全体がより速く感じられます

実際の速さと同じくらい、速さの感じ方が重要です。イージングはこの効果を高めます。200ms の ease-out は、ユーザーがすぐに動きを目にするため、200ms の ease-in より速く感じられます。

スプリングアニメーション

スプリングは実際の物理現象を再現するため、所要時間を基準にしたアニメーションより自然に感じられます。所要時間は固定されておらず、物理パラメーターに応じて動きが収束します。

スプリングを使う場面
  • 慣性のあるドラッグ操作
  • 「生きている」ように感じさせたい要素(AppleのDynamic Islandなど)
  • アニメーションの途中で中断できるジェスチャー
  • 装飾的なマウストラッキング操作
スプリングを使ったマウス操作

視覚的な変化をマウスの位置に直接結び付けると、動きがないため人工的に感じられます。Motion(旧Framer Motion)のuseSpringを使って、値の変化をすぐに反映するのではなく、スプリングのような動きで補間します。

import { useSpring } from 'framer-motion';

// Without spring: feels artificial, instant
const rotation = mouseX * 0.1;

// With spring: feels natural, has momentum
const springRotation = useSpring(mouseX * 0.1, {
  stiffness: 100,
  damping: 10,
});

これは、アニメーションが装飾的であり、機能を果たすものではないからです。銀行アプリの機能的なグラフなら、アニメーションはないほうがよいでしょう。装飾が役立つ場面と邪魔になる場面を見極めてください。

スプリングの設定

Appleのアプローチ(推奨 — 調整しやすい方法):

{ type: "spring", duration: 0.5, bounce: 0.2 }

従来の物理ベースの方法(より細かく制御できます):

{ type: "spring", mass: 1, stiffness: 100, damping: 10 }

バウンスを使う場合は、控えめに(0.1~0.3)してください。ほとんどのUIではバウンスを避けましょう。ドラッグして閉じる操作や、遊び心のあるインタラクションに使ってください。

中断可能であることの利点

スプリングは中断時の速度を維持しますが、CSSアニメーションやキーフレームは最初から再開します。そのため、動作の途中でユーザーが変更する可能性のあるジェスチャーには、スプリングが最適です。展開した項目をクリックした直後にEscapeキーを押すと、スプリングベースのアニメーションは現在の位置から滑らかに反転します。

コンポーネント構築の原則

ボタンの応答性を高める

:activeにtransform: scale(0.97)を追加します。すぐにフィードバックが返るため、UIがユーザーの操作にきちんと反応していると感じられます。

.button {
  transition: transform 160ms ease-out;
}

.button:active {
  transform: scale(0.97);
}

これは、押すことのできるあらゆる要素に当てはまります。スケールは控えめにしてください(0.95~0.98)。

scale(0)からアニメーションさせない

現実世界では、何かが完全に消えたり、再び現れたりすることはありません。scale(0)からアニメーションする要素は、突然どこからともなく現れたように見えます。

不透明度と組み合わせて、scale(0.9)以上から始めましょう。初期スケールがかすかに見えるだけでも、しぼんだ風船に形があるように、表示時の動きがより自然になります。

/* Bad */
.entering {
  transform: scale(0);
}

/* Good */
.entering {
  transform: scale(0.95);
  opacity: 0;
}
ポップオーバーの表示位置をトリガーに合わせる

ポップオーバーは中央からではなく、トリガーから拡大して表示するべきです。デフォルトのtransform-origin: centerは、ほぼすべてのポップオーバーに適していません。**例外:モーダルです。**モーダルは特定のトリガーに固定されず、ビューポートの中央に表示されるため、transform-origin: centerのままにしてください。

/* Radix UI */
.popover {
  transform-origin: var(--radix-popover-content-transform-origin);
}

/* Base UI */
.popover {
  transform-origin: var(--transform-origin);
}

ユーザーが個々の違いに気付くかどうかは重要ではありません。積み重なることで、見えなかった細部が見えるようになります。効果は積み重なります。

ツールチップ:連続してホバーするときは遅延を省く

誤操作による表示を防ぐため、ツールチップは表示前に遅延させるべきです。ただし、ひとつのツールチップが開いている間は、隣接するツールチップにホバーすると、アニメーションなしですぐに開くようにしましょう。最初の遅延の目的を損なうことなく、より素早く感じられます。

.tooltip {
  transition: transform 125ms ease-out, opacity 125ms ease-out;
  transform-origin: var(--transform-origin);
}

.tooltip[data-starting-style],
.tooltip[data-ending-style] {
  opacity: 0;
  transform: scale(0.97);
}

/* Skip animation on subsequent tooltips */
.tooltip[data-instant] {
  transition-duration: 0ms;
}
中断可能なUIにはキーフレームよりCSSトランジションを使う

CSSトランジションは、アニメーションの途中で中断し、目標を変更できます。キーフレームは最初から再開します。トーストの追加や状態の切り替えなど、短時間に繰り返し操作される可能性がある場合は、トランジションを使うとより滑らかになります。

/* Interruptible - good for UI */
.toast {
  transition: transform 400ms ease;
}

/* Not interruptible - avoid for dynamic UI */
@keyframes slideIn {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}
不完全なトランジションをぼかしで目立たなくする

さまざまなイージングや時間を試しても、2つの状態間のクロスフェードがしっくりこない場合は、トランジション中に控えめなfilter: blur(2px)を追加します。

**ぼかしが効果的な理由:**ぼかしがないと、クロスフェード中に古い状態と新しい状態が重なった、別々の2つのオブジェクトが見えます。これは不自然に見えます。ぼかしを加えると、2つの状態が混ざり合って視覚的な隔たりが埋まり、2つのオブジェクトが入れ替わるのではなく、ひとつの滑らかな変化として目に映ります。

ぼかしと、押したときのスケール変化(scale(0.97))を組み合わせて、洗練されたボタンの状態遷移を実現します。

.button {
  transition: transform 160ms ease-out;
}

.button:active {
  transform: scale(0.97);
}

.button-content {
  transition: filter 200ms ease, opacity 200ms ease;
}

.button-content.transitioning {
  filter: blur(2px);
  opacity: 0.7;
}

ぼかしは20px未満にしてください。強いぼかしは、特にSafariでは処理負荷が高くなります。

@starting-style で表示時の状態をアニメーションする

JavaScript を使わずに要素の表示をアニメーションする、最新の CSS の方法です。

.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;

  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}

これは、初回レンダリング後に useEffect を使って mounted: true に設定する、よく使われる React のパターンに代わるものです。ブラウザーが対応している場合は @starting-style を使い、そうでない場合は data-mounted 属性のパターンにフォールバックしてください。

// Legacy pattern (still works everywhere)
useEffect(() => {
  setMounted(true);
}, []);
// <div data-mounted={mounted}>

CSS Transform を使いこなす

パーセンテージを使った translateY

translate() のパーセンテージ値は、要素自身のサイズを基準にします。translateY(100%) を使うと、実際の寸法に関係なく、要素を自身の高さ分だけ移動できます。Sonner がトーストを配置する方法や、Vaul がアニメーション表示する前にドロワーを隠す方法は、この仕組みを利用しています。

/* Works regardless of drawer height */
.drawer-hidden {
  transform: translateY(100%);
}

/* Works regardless of toast height */
.toast-enter {
  transform: translateY(-100%);
}

ハードコードしたピクセル値よりもパーセンテージを優先してください。エラーが起きにくく、コンテンツに合わせて調整されます。

scale() は子要素も拡大縮小する

width/height とは異なり、scale() は要素の子要素も拡大縮小します。ボタンを押したときに拡大縮小すると、フォントサイズ、アイコン、コンテンツも比例して拡大縮小されます。これはバグではなく、機能です。

奥行きを表現する 3D トランスフォーム

transform-style: preserve-3d と rotateX()、rotateY() を組み合わせると、CSS で本格的な 3D 効果を作成できます。軌道を描くアニメーション、コインの回転、奥行きの効果を、すべて JavaScript なしで実現できます。

.wrapper {
  transform-style: preserve-3d;
}

@keyframes orbit {
  from {
    transform: translate(-50%, -50%) rotateY(0deg) translateZ(72px) rotateY(360deg);
  }
  to {
    transform: translate(-50%, -50%) rotateY(360deg) translateZ(72px) rotateY(0deg);
  }
}
transform-origin

すべての要素には、トランスフォームの基準となるアンカーポイントがあります。デフォルトは中央です。インタラクションの起点に合わせて設定してください。

アニメーションに clip-path を使う

clip-path は図形の作成だけに使うものではありません。CSS で最も強力なアニメーションツールの一つです。

inset の形状

clip-path: inset(top right bottom left) は、長方形のクリッピング領域を定義します。それぞれの値は、指定した側から要素を「削り取る」ように働きます。

/* Fully hidden from right */
.hidden {
  clip-path: inset(0 100% 0 0);
}

/* Fully visible */
.visible {
  clip-path: inset(0 0 0 0);
}

/* Reveal from left to right */
.overlay {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 200ms ease-out;
}
.button:active .overlay {
  clip-path: inset(0 0 0 0);
  transition: clip-path 2s linear;
}
完璧な色の遷移を実現するタブ

タブリストを複製します。複製した方を「アクティブ」な状態(背景色とテキスト色を変更)にスタイル設定します。複製したリストをクリップし、アクティブなタブだけが見えるようにします。タブが切り替わったときにクリップをアニメーションさせます。個々の色の遷移のタイミングを調整するだけでは実現できない、シームレスな色の遷移を作れます。

長押しで削除するパターン

色付きのオーバーレイに clip-path: inset(0 100% 0 0) を使います。:active では、線形タイミングで 2s かけて inset(0 0 0 0) に遷移させます。指を離したら、200ms の ease-out で元に戻します。押したときのフィードバックとして、ボタンに scale(0.97) を追加します。

スクロール時の画像表示

clip-path: inset(0 0 100% 0)(下から隠れた状態)で開始します。要素がビューポートに入ったときに inset(0 0 0 0) へアニメーションさせます。IntersectionObserver または Framer Motion の useInView を { once: true, margin: "-100px" } とともに使います。

比較スライダー

2 枚の画像を重ねます。上側の画像を clip-path: inset(0 50% 0 0) でクリップします。ドラッグ位置に応じて右側のインセット値を調整します。追加の DOM 要素は不要で、完全にハードウェアアクセラレーションされます。

ジェスチャーとドラッグ操作

慣性に基づく解除

しきい値を超えるまでドラッグすることを求めないでください。速度を計算します:Math.abs(dragDistance) / elapsedTime。速度が約 0.11 を超えたら、距離に関係なく解除します。すばやく弾くだけで十分です。

const timeTaken = new Date().getTime() - dragStartTime.current.getTime();
const velocity = Math.abs(swipeAmount) / timeTaken;

if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
  dismiss();
}
境界での減衰

ユーザーが自然な境界を越えてドラッグした場合(たとえば、ドロワーがすでに最上部にある状態で上方向にドラッグする場合)は、減衰を適用します。ドラッグする距離が大きくなるほど、要素の移動量を小さくします。現実のものは突然止まるのではなく、まず速度が落ちます。

ドラッグ時のポインターキャプチャ

ドラッグが始まったら、要素がすべてのポインターイベントをキャプチャするように設定します。これにより、ポインターが要素の境界から外れてもドラッグを続けられます。

マルチタッチの保護

最初のドラッグが始まった後は、追加のタッチポイントを無視します。これをしないと、ドラッグ中に指を切り替えたとき、要素が新しい位置に跳ねてしまいます。

function onPress() {
  if (isDragging) return;
  // Start drag...
}
急停止ではなく摩擦を使う

上方向へのドラッグを完全に防ぐのではなく、摩擦を徐々に強めながらドラッグできるようにします。見えない壁にぶつかるよりも自然に感じられます。

パフォーマンスのルール

transform と opacity だけをアニメーションする

これらのプロパティはレイアウトとペイントをスキップし、GPU 上で実行されます。padding、margin、height、width をアニメーションすると、描画処理の 3 つのステップすべてが実行されます。

CSS 変数は継承されます

親要素の CSS 変数を変更すると、すべての子要素のスタイルが再計算されます。項目が多いドロワーで、コンテナーの --swipe-amount を更新すると、スタイルの再計算に大きな負荷がかかります。代わりに、要素の transform を直接更新してください。

// Bad: triggers recalc on all children
element.style.setProperty('--swipe-amount', `${distance}px`);

// Good: only affects this element
element.style.transform = `translateY(${distance}px)`;
Framer Motion のハードウェアアクセラレーションに関する注意点

Framer Motion の省略形プロパティ(x、y、scale)はハードウェアアクセラレーションに対応していません。メインスレッドで requestAnimationFrame を使用します。ハードウェアアクセラレーションを利用するには、完全な transform 文字列を使用してください。

// NOT hardware accelerated (convenient but drops frames under load)
<motion.div animate={{ x: 100 }} />

// Hardware accelerated (stays smooth even when main thread is busy)
<motion.div animate={{ transform: "translateX(100px)" }} />

これは、ブラウザーが同時にコンテンツの読み込みやスクリプトの実行、描画を行っている場合に重要です。Vercel では、ダッシュボードのタブアニメーションに Shared Layout Animations を使用していましたが、ページの読み込み中にフレーム落ちが発生しました。CSS アニメーション(メインスレッド外で実行)に切り替えることで解決しました。

負荷が高いときは CSS アニメーションが JS に勝ります

CSS アニメーションはメインスレッド外で実行されます。ブラウザーが新しいページの読み込みで忙しいと、Framer Motion のアニメーション(requestAnimationFrame を使用)はフレーム落ちします。CSS アニメーションは滑らかなままです。事前に決められたアニメーションには CSS を、動的で中断可能なアニメーションには JS を使用してください。

プログラムから CSS アニメーションを制御するには WAAPI を使用します

Web Animations API を使うと、CSS のパフォーマンスを保ちながら JavaScript で制御できます。ハードウェアアクセラレーションに対応し、中断も可能で、ライブラリも不要です。

element.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }], {
  duration: 1000,
  fill: 'forwards',
  easing: 'cubic-bezier(0.77, 0, 0.175, 1)',
});

アクセシビリティ

prefers-reduced-motion

アニメーションは乗り物酔いのような症状を引き起こすことがあります。動きを減らす設定では、アニメーションをなくすのではなく、数を減らして穏やかにします。理解を助ける不透明度や色のトランジションは維持してください。動きや位置のアニメーションは取り除きます。

@media (prefers-reduced-motion: reduce) {
  .element {
    animation: fade 0.2s ease;
    /* No transform-based motion */
  }
}
const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : '-100%';
タッチデバイスのホバー状態
@media (hover: hover) and (pointer: fine) {
  .element:hover {
    transform: scale(1.05);
  }
}

タッチデバイスでは、タップ時にホバーが発生し、誤検出の原因になります。このメディアクエリーを使って、ホバーアニメーションを制御してください。

Sonner の原則(愛されるコンポーネントを作る)

これらの原則は、Sonner(npm で週あたり 1,300 万回以上ダウンロード)の開発から得られたもので、あらゆるコンポーネントに当てはまります。

  1. 開発者体験が重要です。 フックも、コンテキストも、複雑なセットアップも必要ありません。<Toaster /> を一度挿入し、どこからでも toast() を呼び出せます。導入の手間が少ないほど、多くの人に使われます。

  2. 優れたデフォルト設定は、オプションより重要です。 何もしなくても美しく使えるものを提供しましょう。ほとんどのユーザーはカスタマイズしません。デフォルトのイージング、タイミング、ビジュアルデザインは優れたものであるべきです。

  3. 名前が個性を生み出します。 「Sonner」(フランス語で「鳴る」の意味)は、「react-toast」よりも洗練された印象を与えます。適切な場合には、見つけやすさより覚えやすさを優先しましょう。

  4. エッジケースは目立たないように処理します。 タブが非表示のときはトーストのタイマーを一時停止します。重なったトーストの間を疑似要素で埋め、ホバー状態を維持します。ドラッグ中はポインターイベントを捕捉します。ユーザーがこれらに気づかないことこそ、理想的な状態です。

  5. 動的な UI にはキーフレームではなくトランジションを使用します。 トーストは短時間に次々と追加されます。中断されるとキーフレームは最初から再開しますが、トランジションは滑らかに目標値を切り替えます。

  6. 優れたドキュメントサイトを作りましょう。 ユーザーが製品を試し、操作し、使い始める前に理解できるようにします。すぐ使えるコードスニペットを含むインタラクティブなサンプルは、導入のハードルを下げます。

一貫性が重要です

Sonner のアニメーションが心地よく感じられるのは、体験全体に一貫性があることも理由の一つです。イージングと時間がライブラリの雰囲気に合っています。一般的な UI アニメーションよりわずかに遅く、より洗練された印象にするために ease-out ではなく ease を使用しています。アニメーションのスタイルは、トーストのデザイン、ページのデザイン、名前など、すべてと調和しています。

アニメーションの値を選ぶときは、コンポーネントの個性を考慮してください。遊び心のあるコンポーネントなら、弾むような動きが合うでしょう。プロ向けのダッシュボードなら、きびきびと素早い動きが適しています。動きを雰囲気に合わせましょう。

不透明度と高さの組み合わせ

項目がリスト(Family のドロワーなど)に追加・削除されるときは、不透明度の変化と高さのアニメーションがうまく連動する必要があります。これは試行錯誤が必要になることがよくあります。決まった公式はありません。しっくりくるまで調整してください。

翌日に作業を見直す

新鮮な目でアニメーションを見直しましょう。開発中に見落としていた不自然な点に、翌日になって気づくことがあります。アニメーションをスローモーションまたはコマ送りで再生し、通常の速度では見えないタイミングの問題を見つけましょう。

入場と退場のタイミングを非対称にする

押す動作は、慎重な操作が必要な場合はゆっくりにします(押し続けて削除する場合:2s linear)。一方、離す動作は常に素早くします(200ms ease-out)。このパターンは幅広く適用できます。ユーザーが判断する場面ではゆっくり、システムが応答する場面では素早くします。

/* Release: fast */
.overlay {
  transition: clip-path 200ms ease-out;
}

/* Press: slow and deliberate */
.button:active .overlay {
  transition: clip-path 2s linear;
}

スタッガーアニメーション

複数の要素が同時に表示される場合は、表示タイミングにずれをつけましょう。各要素は、前の要素のアニメーションが始まってから少し遅れてアニメーションします。すべてが一度に表示されるよりも自然な、連続して広がるような効果が生まれます。

.item {
  opacity: 0;
  transform: translateY(8px);
  animation: fadeIn 300ms ease-out forwards;
}

.item:nth-child(1) {
  animation-delay: 0ms;
}
.item:nth-child(2) {
  animation-delay: 50ms;
}
.item:nth-child(3) {
  animation-delay: 100ms;
}
.item:nth-child(4) {
  animation-delay: 150ms;
}

@keyframes fadeIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

スタッガーの遅延は短く保ちます(項目間で30~80ms)。遅延が長いと、インターフェースの動作が遅く感じられます。スタッガーは装飾的なものです。スタッガーアニメーションの再生中に、操作を妨げないでください。

アニメーションのデバッグ

スローモーションでのテスト

アニメーションを低速で再生し、通常の速度では見えない問題を見つけましょう。再生時間を一時的に通常の2~5倍にするか、ブラウザーのDevToolsアニメーションインスペクターを使って再生速度を落とします。

スローモーションで確認する点:

  • 色は滑らかに遷移しますか。それとも、異なる2つの状態が重なって見えますか?
  • イージングは適切ですか。それとも、急に始まったり止まったりしますか?
  • transform-originは正しく設定されていますか。それとも、要素が誤った位置を起点に拡大縮小していますか?
  • 複数のアニメーション対象プロパティ(opacity、transform、color)は同期していますか?
コマ送りでの確認

Chrome DevTools(Animationsパネル)でアニメーションをコマ送りしながら確認します。これにより、連動するプロパティ間のタイミングの問題が明らかになります。通常の速度では確認できません。

実機でテストする

ドロワーやスワイプジェスチャーなどのタッチ操作は、実機でテストします。USBでスマートフォンを接続し、IPアドレスを使ってローカルの開発サーバーにアクセスし、Safariのリモート開発者ツールを使います。Xcode Simulatorも代替手段ですが、ジェスチャーのテストには実機のほうが適しています。

レビューチェックリスト

UIコードをレビューするときは、次の点を確認します。

問題修正方法
transition: all対象のプロパティを明示します:transition: transform 200ms ease-out
scale(0)による入場アニメーションscale(0.95)とopacity: 0で開始します
UI要素にease-inを使用ease-outまたはカスタムカーブに切り替えます
ポップオーバーにtransform-origin: centerを使用トリガーの位置に設定するか、Radix/Base UIのCSS変数を使います(モーダルは対象外です。中央のままにします)
キーボード操作にアニメーションを適用アニメーションを完全に削除します
UI要素の再生時間が300msを超える150~250msに短縮します
メディアクエリーのないホバーアニメーション@media (hover: hover) and (pointer: fine)を追加します
頻繁にトリガーされる要素にキーフレームを使用中断可能なCSSトランジションを使います
負荷時にFramer Motionのx/yプロパティを使用ハードウェアアクセラレーションのためにtransform: "translateX()"を使います
入場と退場のトランジション速度が同じ退場を入場より速くします(例:入場2s、退場200ms)
すべての要素が一度に表示されるスタッガーの遅延を追加します(項目間で30~80ms)

コメント