# @keyframes の命名規則と設計パターン このドキュメントは [`SKILL.md`](../SKILL.md) の Step 2D で参照される、`@keyframes` の詳細な命名規則と設計パターンをまとめたものである。 ## 1. 命名の基本ルール ### dashed ident の必須化 `@keyframes` の名前は必ず dashed ident(`--` で始まる名前)を使用する。 ```css /* OK */ @keyframes --fade-in { ...; } @keyframes --slide-up { ...; } /* NG: dashed ident でない */ @keyframes fadeIn { ...; } @keyframes slide-up { ...; } ``` 理由: - 近年の CSS では、ユーザー定義の名前を明確に区別するために `--` を強制する流れがある(例:`anchor-name`、`animation-timeline` など)。 - 「そのプロパティでは `--` が必須か?」を個別に覚える必要が減る。 - 将来 CSS 標準側のキーワードが増えても、衝突リスクを下げられる。 - 自分が定義した名前だと即座に判別できる。 ### スコープ別の命名規則 | スコープ | 命名形式 | 配置場所 | 例 | | ------------------ | -------------------------------------- | ----------------------------- | ------------------------------------------------------------ | | グローバル | `--{animation-name}` | `base/keyframes.css` | `--fade-in`, `--scale-from`, `--translate-from` | | コンポーネント固有 | `--{component-name}--{animation-name}` | コンポーネントの CSS ファイル | `--shared-dialog--backdrop-fade`, `--shared-toast--slide-in` | - グローバル keyframes はプロジェクト全体で再利用される汎用アニメーションのみを対象とする - コンポーネント固有の keyframes は、そのコンポーネントの CSS 内に定義する - 迷ったらコンポーネント固有として定義し、再利用が明確になった時点(3回利用されるなど)でグローバルに昇格させる ## 2. グローバル keyframes の設計 ### 配置と管理 グローバル keyframes は全て `base/keyframes.css` に集約する。 ``` styles/ ├── base/ │ ├── keyframes.css ← グローバル keyframes はここに全て配置 │ └── ... ├── components/ │ ├── shared-dialog.css ← コンポーネント固有の keyframes はここ │ └── ... └── tokens/ └── animation.css ``` ### ユーティリティクラスのように扱う グローバル keyframes は、ユーティリティクラスと同じ設計思想で扱う。1 つの keyframes が 1 つのプロパティ変化だけを担当し、組み合わせによって複雑なアニメーションを構成する。 #### 原則 - 1 keyframes = 1 プロパティの変化に限定する(`--fade-in` は `opacity` のみ、`--scale-from` は `scale` のみ) - 複数プロパティを同時に動かしたい場合は、`animation-name` のカンマ区切りで複数の keyframes を組み合わせる - 各 keyframes は独立して意味を持ち、単体でも使えること - 組み合わせ時に `animation-duration` や `animation-timing-function` を個別に調整できるため、プロパティごとに最適なイージングやタイミングを選び分けられる #### OK: 単一責務の keyframes を組み合わせる ```css /* 利用側: 組み合わせて使う */ .dropdown { --scale-from--x-value: 0.95; --scale-from--y-value: 0.95; animation-name: --fade-in, --scale-from; animation-duration: 200ms; animation-timing-function: var(--ease--out-quint), var(--ease--out-expo); animation-fill-mode: both; } .toast { --translate-from--y-value: 100%; animation-name: --fade-in, --translate-from; animation-duration: 200ms, 300ms; animation-timing-function: var(--ease--out-quint); animation-fill-mode: both; } ``` #### NG: 複数プロパティを 1 つの keyframes にまとめる ```css /* NG: グローバル keyframes が複数プロパティを持つ */ @keyframes --fade-in-and-scale { from { opacity: 0; scale: 0.95; } } ``` この設計では以下の問題が生じる: - `opacity` と `scale` のイージングやタイミングを個別に制御できない - `opacity` のみ、`scale` のみが欲しい場面で再利用できない - API 的カスタムプロパティの命名が複雑になる(どのプロパティに対する値なのか曖昧になる) - コンポーネントごとに微妙に異なる組み合わせが必要になった時点で、グローバルの意味を失う ### to/from で完結しないものはグローバルに定義しない グローバル keyframes は `from` / `to` の 2 フレームで完結するものに限定する。中間フレーム(`25%`, `50%` など)を含む keyframes はコンポーネント固有として定義すること。 #### 理由 - 中間フレームを持つ keyframes は、特定の UI パターンや視覚効果に強く結びついている。例えば「50% で一度色が変わる」動きは汎用的ではなく、その挙動を必要とするコンポーネントなどに閉じるべきである - `from` / `to` のみの keyframes は「開始値と終了値」だけを定義するため、API 的カスタムプロパティとの相性が良い。中間フレームが入ると注入すべき値が増え、API の複雑さが跳ね上がる - ユーティリティクラス的な単一責務の設計原則と合致する。`from` / `to` は「A から B への変化」という最小単位であり、それ以上の複雑さはコンポーネント固有の責務である #### グローバルに置いてよいもの ```css /* OK: from/to の 2 フレームで完結 */ @keyframes --fade-in { from { opacity: var(--fade-in--from-value, 0); } } @keyframes --scale-from { from { scale: var(--scale-from--x-value, 1) var(--scale-from--y-value, 1); } } ``` #### グローバルに置いてはいけないもの ```css /* NG: 中間フレームを含むのでローカルで定義すべき */ @keyframes --pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } ``` ### from/to を省略可能な場合は省略する CSS の仕様では、`@keyframes` の `from` または `to` が省略された場合、アニメーション対象要素がその時点で持っている値が自動的に補完される。この仕様を活用し、自明な `from` や `to` は書かないことを原則とする。 #### 理由 - 省略することで要素が現在持っている値を尊重し、カスケーディングに沿った自然なアニメーションが実行される - `from` や `to` に値をハードコードすると、要素の現在値が想定と異なる場合にジャンプが発生する - API 的カスタムプロパティで注入すべき値が減り、keyframes の設計がシンプルになる #### 具体例: `to` の省略 ```css /* OK: to を省略 — 要素の現在の opacity に向かってアニメーションする */ @keyframes --fade-in { from { opacity: var(--fade-in--from-value, 0); } } ``` `opacity: 0.8` の要素に適用すれば `0 → 0.8`、`opacity: 1` なら `0 → 1` に自然に収束する。 ```css /* NG: to { opacity: 1 } を明記してしまう */ @keyframes --fade-in { from { opacity: 0; } to { opacity: 1; } } ``` この場合、`opacity: 0.8` の要素に `animation-fill-mode: both` で適用すると、アニメーション終了時に `opacity` が `1` に強制され、アニメーション後に `0.8` に戻るフラッシュが発生する。`animation-fill-mode: forwards` なら `1` のまま留まり、要素本来の `0.8` が上書きされてしまう。 #### 具体例: `from` の省略 ```css /* OK: from を省略 — 要素の現在の opacity から 0 に向かう */ @keyframes --fade-out { to { opacity: var(--fade-out--to-value, 0); } } ``` `opacity: 0.5` の半透明要素に適用すれば `0.5 → 0`、`opacity: 1` なら `1 → 0` になる。`from { opacity: 1 }` をハードコードしていた場合、半透明の要素が一瞬 `1` にジャンプしてから消えるという不自然な挙動になる。 #### 判断基準 | 状況 | from/to | | ----------------------------------------------------- | -------------------------------------------------- | | 「ゼロ / 非表示」からの出現(fade-in, scale-from 等) | `from` のみ記述、`to` は省略 | | 「ゼロ / 非表示」への退場(fade-out 等) | `to` のみ記述、`from` は省略 | | 開始値と終了値の両方を厳密に制御する必要がある | 両方記述(ただしローカル定義にすべきケースが多い) | | 無限ループ(rotate 等) | `to` のみ記述 | ### API 的カスタムプロパティパターン グローバル keyframes は汎用的に再利用可能であるべきである。そのため、条件によってアニメーションの起点や方向を変えたいケースでは、keyframes 内部にカスタムプロパティの参照を埋め込み、利用側から値を注入する。 #### カスタムプロパティの命名規則 keyframes 用の API 的カスタムプロパティは `--{keyframes-name}--{value-name}` の形式で命名する。 `--` の接頭辞は keyframes 名に含まれるため、ローカルカスタムプロパティで必須としているアンダースコア始まり(`--_`)は不要である。 ``` --{keyframes-name}--{value-name} 例: --scale-from--x-value, --translate-from--y-value ``` #### 基本パターン: `--scale-from` ```css /* base/keyframes.css */ @keyframes --scale-from { from { scale: var(--scale-from--x-value, 1) var(--scale-from--y-value, 1); } } ``` ```css [popover] { --scale-from--x-value: 0.96; --scale-from--y-value: 0.96; animation-name: --scale-from; animation-duration: 200ms; animation-timing-function: var(--ease--out-quint); animation-fill-mode: both; } /* 配置方向に応じて起点を変える */ [popover][data-placement="top"] { --scale-from--y-value: 1.04; } ``` #### 基本パターン: `--translate-from` ```css /* base/keyframes.css */ @keyframes --translate-from { from { translate: var(--translate-from--x-value, 0) var( --translate-from--y-value, 0 ); } } ``` ```css .toast { --translate-from--y-value: 100%; animation-name: --translate-from; animation-duration: 300ms; animation-timing-function: var(--ease--out-quint); animation-fill-mode: both; } /* 位置に応じて方向を変える */ .toast[data-position="top"] { --translate-from--y-value: -100%; } ``` #### 基本パターン: `--fade-in` ```css /* base/keyframes.css */ @keyframes --fade-in { from { opacity: var(--fade-in--from-value, 0); } } @keyframes --fade-out { to { opacity: var(--fade-out--to-value, 0); } } ``` #### 複合パターン: 複数の keyframes を組み合わせる `animation` プロパティはカンマ区切りで複数指定できる。グローバル keyframes を組み合わせることで、コンポーネント固有の keyframes を定義せずに済む場合がある。 ```css /* fade + scale の組み合わせ */ .dropdown { --scale-from--x-value: 0.95; --scale-from--y-value: 0.95; animation-name: --fade-in, --scale-from; animation-duration: 200ms; animation-timing-function: var(--ease--out-quint), var(--ease--out-expo); animation-fill-mode: both; } ``` #### scroll-linked animation での活用: `paused` + `both` パターン `IntersectionObserver` による状態トグルで発火させる scroll-linked animation では、グローバル keyframes と `animation-play-state: paused` を組み合わせることで、初期非表示と発火を一元管理できる。 ```css .reveal-item { --translate-from--y-value: 20px; animation-name: --fade-in, --translate-from; animation-duration: 600ms; animation-timing-function: var(--ease--out-quint); animation-fill-mode: both; animation-play-state: paused; /* アクティブ時に発火 */ &[data-revealed] { animation-play-state: initial; } } ``` - `animation-fill-mode: both` により、`paused` 状態でも keyframes の `from` フレームが適用される。これにより要素は keyframes 上の開始地点(`opacity: 0`, `translate: 0 20px`)で描画される - アクティブ化(属性付与等)で `animation-play-state` を `initial`(= `running`)に戻すだけでアニメーションが発火する - keyframes の外に `opacity: 0` 等を別途記述する必要がなく、keyframes が初期状態の単一の情報源(single source of truth)となる ## 3. ローカルの keyframes ### 命名と配置 コンポーネントや affordance の keyframes は、そのコンポーネントの CSS ファイル内に定義し、`--{component-name}--{animation-name}` の形式で命名する。 ```css /* affordance/arrows.css */ @keyframes --arrows--fill-steps { from, to { fill: var(--color--primary); } 50% { fill: transparent; } } @scope (.arrows) to (:scope > *) { :scope { fill: none; stroke: var(--color--primary); animation-name: --arrows--fill-steps; animation-duration: 200ms; animation-timing-function: steps(1); animation-direction: alternate; animation-iteration-count: infinite; } } ``` ### グローバル keyframes との使い分け | 状況 | 選択 | | ------------------------------------------------------------ | ----------------------------------------------- | | 汎用的な動き(fade, scale, translate)で、起点だけ変えたい | グローバル keyframes + API 的カスタムプロパティ | | コンポーネント固有の複数プロパティを同時にアニメーションする | ローカル keyframes | | 中間フレーム(`50%` など)が必要な複雑なアニメーション | ローカル keyframes | | backdrop や特殊な疑似要素のアニメーション | ローカル keyframes | ## 4. `@keyframes` と `transition` / `@starting-style` の使い分け `@keyframes` を使うべきか、`transition` + `@starting-style` を使うべきかの判断基準。 | 要件 | 推奨手法 | | -------------------------------------------------------- | ------------------------------------------------------------------------ | | `display: none` からの出現で、退場もアニメーションしたい | `transition` + `@starting-style` + `transition-behavior: allow-discrete` | | 出現のみアニメーションし、退場は即時でよい | `@keyframes` animation | | 中間フレーム(`25%`, `50%` など)が必要 | `@keyframes` animation | | JS から動的に制御したい(pause, reverse, cancel) | Web Animations API(`element.animate()`) | | 中断・逆再生が頻発する | `transition` | | 複数プロパティを異なるタイミングで段階的に動かしたい | `@keyframes` animation、または `transition-delay` の組み合わせ | | scroll-linked で初期非表示 → トグル発火 | `@keyframes` + `animation-play-state: paused` / `initial` |