メインコンテンツまでスキップ
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

トランジションのベストプラクティス

問題

CSSトランジションは仕上げを加え、ユーザーが状態変化を理解する助けになります。しかし、AIエージェントは一貫して同じミスを犯します:レイアウト再計算を引き起こす高コストなプロパティ(widthheightmargin など)をトランジションしたり、汎用的な transition: all 0.3s ease を使用したり、display やその他の離散プロパティをトランジションするための transition-behavior: allow-discrete プロパティを考慮しなかったりします。その結果、カクつくアニメーション、パフォーマンスの低下、スムーズなUIトランジションの機会損失につながります。

解決方法

可能な限り 低コストなプロパティtransformopacity)のみをトランジションし、all ではなく具体的なプロパティリストを使用し、適切なイージングカーブを選択し、エントリー/エグジットアニメーションには transition-behavior: allow-discrete@starting-style を活用しましょう。

パフォーマンスの階層

  1. 低コスト(コンポジターのみ)transformopacity — GPUコンポジタースレッドで実行され、レイアウトもペイントも発生しません。
  2. 中程度(ペイントのみ)background-colorcolorbox-shadow — リペイントは発生しますがレイアウトは発生しません。
  3. 高コスト(レイアウトトリガー)widthheightmarginpaddingtopleft — 完全なレイアウト再計算が発生します。
トランジション比較 — ホバーでスムーズ 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)を使用する代わりに widthheightmargintopleft をトランジションしてしまいます。
  • すべてに linearease を使用する:インタラクションの種類にイージングを合わせていません。入場する要素には ease-out を、退場する要素には ease-in を使いましょう。
  • 長すぎるデュレーション:単純な状態変化に 0.5s 以上を使用しています。マイクロインタラクションは 100-200ms にすべきです。
  • transition-behavior: allow-discrete を知らないdisplayallow-discrete@starting-style でトランジションする代わりに、JavaScript でディレイ付きのクラストグルを使用してしまいます。
  • @starting-style を忘れるtransition-behavior: allow-discrete を使用しながら @starting-style を省略するため、エントリーアニメーションにトランジション元となる開始状態がありません。
  • ページロード時にトランジションが発火する:トランジションのスコープを適切に設定せず、ページの初回レンダリング時に発火してしまい、気が散るアニメーションを引き起こします。

使い分け

  • 状態変化:インタラクティブ要素のホバー、フォーカス、アクティブ、開閉状態に使います。
  • transformopacity:モーションには常にこれらを優先しましょう。コンポジタースレッドで実行され、カクつきが発生しません。
  • transition-behavior: allow-discrete:ツールチップ、ポップオーバー、ダイアログ、ドロップダウンメニューで display: none から display: block へのアニメーションに使います。
  • @starting-style:ページに入場する要素や初めて表示される要素の初期状態を定義するために使います。
  • 複雑なシーケンスには使わない:マルチステップのシーケンスにはCSS @keyframes アニメーションを使いましょう。トランジションは2状態間の変化を扱います。

参考リンク