CSS @starting-style でページ読み込み時のアニメーションを実現する

2026.03.13 09:00
2026.03.16 13:32
CSS @starting-style でページ読み込み時のアニメーションを実現する

CSSでアニメーションを実装するとき、display: none から要素を表示する際にトランジションが効かなくて困ったことないですか?モーダルやドロップダウンの表示アニメーションで何度もこの壁にぶつかってきました。従来はJavaScriptで無理やりクラスを付け替えたり、visibilityopacity を組み合わせたりと、結構回りくどい方法で対処してたんですよね。

そんな長年の課題を解決してくれるのが、CSS の @starting-style です。今回は基本的な使い方から実践的なパターンまで一通り試してみました。

サンプルページを見る(モーダル・フェードイン・ポップオーバー・TODO追加のデモ)

display: none からのトランジションができなかった問題

まずは、なぜ display: none からのトランジションが効かないのかをおさらい。

CSSのトランジションは「開始値」と「終了値」の間を補間して滑らかに変化させる仕組みです。でも display: none が設定された要素はレンダリングツリーから完全に除外されるので、ブラウザは「開始値」を持っていない。つまり、どこからどこへ変化させればいいのかわからない状態になります。

たとえば、以下のようなコードではトランジションは動作しません。

.modal {
  display: none;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.modal.is-open {
  display: block;
  opacity: 1;
  /* opacity のトランジションは効かない */
}

display: nonedisplay: block に切り替わった瞬間に opacity: 1 が即座に適用されてしまい、フェードインにはならない。これが従来の仕様上の制約でした。

@starting-style の基本構文

@starting-style は、要素がDOMに挿入されたとき、または display: none から表示状態に切り替わったときに適用される「初期スタイル」を定義するアットルールです。ブラウザはこの初期スタイルを起点としてトランジションを開始してくれます。

基本的な書き方は2パターンあります。

パターン1: ネストして書く方法

.element {
  opacity: 1;
  transition: opacity 0.3s ease;

  @starting-style {
    opacity: 0;
  }
}

パターン2: 独立して書く方法

@starting-style {
  .element {
    opacity: 0;
  }
}

.element {
  opacity: 1;
  transition: opacity 0.3s ease;
}

どちらも同じ結果になります。個人的にはネストする書き方のほうが、対象要素との対応がわかりやすくて好みですね。

実例1: モーダルの表示アニメーション

実用的な例として、モーダルダイアログの表示アニメーションを実装してみました。display: none から display: block への切り替えでフェードイン+スケールアップするパターンです。

.modal-overlay {
  display: none;
  opacity: 0;
  transition: opacity 0.3s ease, display 0.3s ease allow-discrete;

  @starting-style {
    opacity: 0;
  }
}

.modal-overlay.is-open {
  display: block;
  opacity: 1;
}

.modal-content {
  transform: scale(0.9) translateY(20px);
  opacity: 0;
  transition: transform 0.3s ease, opacity 0.3s ease;

  @starting-style {
    transform: scale(0.9) translateY(20px);
    opacity: 0;
  }
}

.modal-overlay.is-open .modal-content {
  transform: scale(1) translateY(0);
  opacity: 1;
}

ポイントは transition プロパティに display 0.3s ease allow-discrete を含めているところ。これについては後述する transition-behavior のセクションで詳しく書きます。

実例2: ページ読み込み時のフェードイン

@starting-style の面白いところは、ページの初回読み込み時にも機能する点です。要素がDOMに挿入されるタイミングで @starting-style のスタイルが適用されるので、ページロード時のエントリーアニメーションがCSSだけで実現できます。

.fade-in-section {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.6s ease, transform 0.6s ease;

  @starting-style {
    opacity: 0;
    transform: translateY(30px);
  }
}

/* 複数セクションに遅延をつけて順番にフェードイン */
.fade-in-section:nth-child(2) {
  transition-delay: 0.15s;
}

.fade-in-section:nth-child(3) {
  transition-delay: 0.3s;
}

.fade-in-section:nth-child(4) {
  transition-delay: 0.45s;
}

これまで Intersection Observer や DOMContentLoaded イベントでクラスを付け替えていた処理が、CSSだけで書けるのはめちゃくちゃありがたい。transition-delay を使えば、セクションごとに時間差で順番にフェードインさせることもできます。

実例3: popover との組み合わせ

HTML の popover 属性は、要素の表示/非表示を display: none で制御します。まさに @starting-style と組み合わせるのにぴったりですね。

<button popovertarget="my-popover">メニューを開く</button>

<div id="my-popover" popover>
  <p>ポップオーバーの内容</p>
</div>
[popover] {
  opacity: 1;
  transform: translateY(0) scale(1);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease,
    display 0.25s ease allow-discrete,
    overlay 0.25s ease allow-discrete;

  @starting-style {
    opacity: 0;
    transform: translateY(-10px) scale(0.95);
  }
}

/* 閉じるときのアニメーション(トップレイヤーから退出時) */
[popover]:not(:popover-open) {
  opacity: 0;
  transform: translateY(-10px) scale(0.95);
}

overlay プロパティもトランジションに含めているのがポイントです。popover はトップレイヤーに表示されるので、overlay のトランジションを指定しないと閉じるときにアニメーション途中でトップレイヤーから即座に外れてしまいます。

transition-behavior との併用

@starting-style を使う際にセットで覚えておきたいのが transition-behavior プロパティです。

displayoverlay のようなプロパティは、通常の連続的な値(数値や色など)とは異なり、離散的(discrete)な値を持ちます。こうした離散的プロパティをトランジションさせるには、transition-behavior: allow-discrete を指定する必要があります。

/* ショートハンドで書く場合 */
.element {
  transition: opacity 0.3s ease, display 0.3s ease allow-discrete;
}

/* ロングハンドで書く場合 */
.element {
  transition-property: opacity, display;
  transition-duration: 0.3s;
  transition-timing-function: ease;
  transition-behavior: normal, allow-discrete;
}

allow-discrete を指定すると、display の値はトランジション期間の終了時に切り替わるようになります。つまり、display: blockdisplay: none への変化は、opacity などのトランジションがすべて完了した後に実行される。これにより、閉じるアニメーションもスムーズに動作します。

ブラウザ対応状況(2026年時点)

2026年3月時点で、@starting-style の対応状況は以下の通りです。

  • Chrome / Edge: 117以降で対応(2023年9月〜)
  • Firefox: 129以降で対応(2024年7月〜)
  • Safari: 17.5以降で対応(2024年5月〜)

主要ブラウザすべてで対応済みなので、2026年現在ではプロダクション環境でも安心して使えますね。transition-behavior: allow-discrete も同時期に各ブラウザで対応しているので、合わせて利用できます。

@supports でのフォールバック

とはいえ、古いブラウザも考慮する必要がある場面もあるかもしれません。@supports を使えば、@starting-style に対応しているかどうかで処理を分岐できます。

/* @starting-style 対応ブラウザ向け */
@supports (transition-behavior: allow-discrete) {
  .modal {
    display: none;
    opacity: 0;
    transition:
      opacity 0.3s ease,
      display 0.3s ease allow-discrete;

    @starting-style {
      opacity: 0;
    }
  }

  .modal.is-open {
    display: block;
    opacity: 1;
  }
}

/* 非対応ブラウザ向けフォールバック */
@supports not (transition-behavior: allow-discrete) {
  .modal {
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.3s ease, visibility 0.3s ease;
  }

  .modal.is-open {
    visibility: visible;
    opacity: 1;
  }
}

フォールバック側では、従来の visibility + opacity パターンを使っています。visibility: hiddendisplay: none と違ってレンダリングツリーに残るため、トランジションが効くからですね。ただし、visibility: hidden はレイアウト上のスペースを占有する点は注意です。

まとめ

@starting-style によって、CSSだけで display: none からのトランジションが実現できるようになりました。これまでJavaScriptに頼っていたエントリーアニメーションの多くが、純粋なCSSで書けるのはかなり大きいですね。

  • @starting-style で要素の初期スタイルを定義する
  • transition-behavior: allow-discretedisplay のトランジションを有効にする
  • ページ読み込み時のアニメーションもCSSだけで対応可能
  • popover やダイアログとの相性が良い
  • 2026年現在、主要ブラウザすべてで対応済み

サンプルページで実際の動きを確認する

今回は以上です!