トランジションのベストプラクティス
問題
CSSトランジションは仕上げを加え、ユーザーが状態変化を理解する助けになります。しかし、AIエージェントは一貫して同じミスを犯します:レイアウト再計算を引き起こす高コストなプロパティ(width、height、margin など)をトランジションしたり、汎用的な transition: all 0.3s ease を使用したり、display やその他の離散プロパティをトランジションするための transition-behavior: allow-discrete プロパティを考慮しなかったりします。その結果、カクつくアニメーション、パフォーマンスの低下、スムーズなUIトランジションの機会損失につながります。
解決方法
可能な限り 低コストなプロパティ(transform、opacity)のみをトランジションし、all ではなく具体的なプロパティリストを使用し、適切なイージングカーブを選択し、エントリー/エグジットアニメーションには transition-behavior: allow-discrete と @starting-style を活用しましょう。
パフォーマンスの階層
- 低コスト(コンポジターのみ):
transform、opacity— GPUコンポジタースレッドで実行され、レイアウトもペイントも発生しません。 - 中程度(ペイントのみ):
background-color、color、box-shadow— リペイントは発生しますがレイアウトは発生しません。 - 高コスト(レイアウトトリガー):
width、height、margin、padding、top、left— 完全なレイアウト再計算が発生します。
トランジション比較 — ホバーでスムーズ vs カクつきを確認
コード例
適切なプロパティをトランジションする
/* Good: transform and opacity are cheap */
.card {
transition: transform 0.2s ease, opacity 0.2s ease;
}
@media (hover: hover) {
.card:hover {
transform: translateY(-4px);
opacity: 0.95;
}
}
/* Bad: animating width triggers layout recalculation */
.card-bad {
transition: width 0.3s ease, height 0.3s ease;
}
高コストなプロパティのトランジションは transform の等価物に置き換えましょう:
/* Instead of transitioning width */
.expandable {
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.expandable.open {
transform: scaleX(1);
}
/* Instead of transitioning top/left */
.slide-in {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.slide-in.visible {
transform: translateX(0);
}
具体的に指定する — transition: all は避ける
/* Bad: transitions every property change, including unintended ones */
.element {
transition: all 0.3s ease;
}
/* Good: only transition what you intend */
.element {
transition: background-color 0.15s ease, transform 0.2s ease;
}
イージング関数の選び方
/* Default ease — good general purpose */
.fade {
transition: opacity 0.2s ease;
}
/* ease-out — element arriving (enters fast, decelerates) */
.slide-enter {
transition: transform 0.3s ease-out;
}
/* ease-in — element leaving (starts slow, accelerates) */
.slide-exit {
transition: transform 0.3s ease-in;
}
/* ease-in-out — continuous motion (both ends decelerate) */
.move {
transition: transform 0.4s ease-in-out;
}
/* Custom cubic-bezier for a snappy, natural feel */
.bounce {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Custom ease-out for UI interactions */
.interact {
transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
}
デュレーションのガイドライン
/* Micro-interactions: 100-200ms */
.button {
transition: background-color 0.15s ease;
}
/* State changes: 200-300ms */
.panel {
transition: transform 0.25s ease-out;
}
/* Complex or large animations: 300-500ms */
.modal-backdrop {
transition: opacity 0.35s ease;
}
transition-behavior: allow-discrete で display をトランジションする
従来、display: none は離散プロパティであるためトランジションできませんでした。transition-behavior: allow-discrete プロパティと @starting-style を組み合わせることで、これが可能になります。
.tooltip {
/* Final visible state */
opacity: 1;
transform: translateY(0);
display: block;
/* Transition including discrete display change */
transition:
opacity 0.2s ease,
transform 0.2s ease,
display 0.2s allow-discrete;
/* Starting state for entry animation */
@starting-style {
opacity: 0;
transform: translateY(-4px);
}
}
.tooltip[hidden] {
/* Exit state */
opacity: 0;
transform: translateY(-4px);
display: none;
}
Popover のエントリー/エグジットアニメーション
[popover] {
/* Final open state */
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.25s ease,
transform 0.25s ease,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
/* Entry animation starting state */
@starting-style {
opacity: 0;
transform: translateY(8px) scale(0.96);
}
}
/* Exit state */
[popover]:not(:popover-open) {
opacity: 0;
transform: translateY(8px) scale(0.96);
}
Dialog とバックドロップのトランジション
dialog {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease,
overlay 0.3s allow-discrete,
display 0.3s allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(16px);
}
}
dialog:not([open]) {
opacity: 0;
transform: translateY(16px);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
opacity: 1;
transition:
opacity 0.3s ease,
display 0.3s allow-discrete;
@starting-style {
opacity: 0;
}
}
スタガードトランジション
.list-item {
opacity: 0;
transform: translateY(8px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.list-item.visible {
opacity: 1;
transform: translateY(0);
}
.list-item:nth-child(1) { transition-delay: 0ms; }
.list-item:nth-child(2) { transition-delay: 50ms; }
.list-item:nth-child(3) { transition-delay: 100ms; }
.list-item:nth-child(4) { transition-delay: 150ms; }
AIがよくやるミス
transition: allを使用する:レイアウトをトリガーするプロパティを含むすべてのプロパティをトランジションしてしまい、パフォーマンス問題と意図しない視覚的変化を引き起こします。- 高コストなプロパティをアニメーションする:
transform(translate、scale)を使用する代わりにwidth、height、margin、top、leftをトランジションしてしまいます。 - すべてに
linearやeaseを使用する:インタラクションの種類にイージングを合わせていません。入場する要素にはease-outを、退場する要素にはease-inを使いましょう。 - 長すぎるデュレーション:単純な状態変化に
0.5s以上を使用しています。マイクロインタラクションは100-200msにすべきです。 transition-behavior: allow-discreteを知らない:displayをallow-discreteと@starting-styleでトランジションする代わりに、JavaScript でディレイ付きのクラストグルを使用してしまいます。@starting-styleを忘れる:transition-behavior: allow-discreteを使用しながら@starting-styleを省略するため、エントリーアニメーションにトランジション元となる開始状態がありません。- ページロード時にトランジションが発火する:トランジションのスコープを適切に設定せず、ページの初回レンダリング時に発火してしまい、気が散るアニメーションを引き起こします。
使い分け
- 状態変化:インタラクティブ要素のホバー、フォーカス、アクティブ、開閉状態に使います。
transformとopacity:モーションには常にこれらを優先しましょう。コンポジタースレッドで実行され、カクつきが発生しません。transition-behavior: allow-discrete:ツールチップ、ポップオーバー、ダイアログ、ドロップダウンメニューでdisplay: noneからdisplay: blockへのアニメーションに使います。@starting-style:ページに入場する要素や初めて表示される要素の初期状態を定義するために使います。- 複雑なシーケンスには使わない:マルチステップのシーケンスにはCSS
@keyframesアニメーションを使いましょう。トランジションは2状態間の変化を扱います。
参考リンク
- Using CSS Transitions — MDN
- transition-behavior — MDN
- An Interactive Guide to CSS Transitions — Josh W. Comeau
- Ten Tips for Better CSS Transitions and Animations — Josh Collinsworth
- Transitioning Top-Layer Entries and the Display Property — Smashing Magazine
- Four New CSS Features for Smooth Entry and Exit Animations — Chrome for Developers