12k
All articles

JavaScriptのハックなしで`display: none`をアニメーションさせる方法

CSSでdisplay noneをallow-discreteと@starting-styleでアニメーション化し、dialogとpopoverのoverlay対応とブラウザ対応も解説。

OpenReplay Team
OpenReplay Team
JavaScriptのハックなしで`display: none`をアニメーションさせる方法

CSSのみで要素をdisplay: noneへ、またはそこからアニメーションさせるには、transitionallow-discreteキーワードを付けてdisplayを追加し、@starting-styleルールにエントリー状態を記述します。JavaScriptもsetTimeouttransitionendリスナーも不要です。これを実現するために2つの機能が実装されました。transition-behavior: allow-discretedisplayのような離散プロパティをトランジションに参加させ、@starting-styleはブラウザに「開く前」の状態を提供してアニメーションの起点とします。このガイドでは、コピー&ペーストですぐに使えるエグジット・エントリーのレシピ、overlayの落とし穴を含むdialog/popoverのトップレイヤーへの対応、そして現在のブラウザサポート状況について率直に解説します。

重要なポイント

  • transitionショートハンドにallow-discreteを付けてdisplayを含め、@starting-styleブロックにエントリー状態を定義することで、JavaScriptなしでdisplayをアニメーションできます。
  • displayは離散プロパティです。allow-discreteを使うと、エントリー時はブラウザが0%のタイミングで表示状態に切り替え、エグジット時は100%のタイミングでnoneに切り替えるため、アニメーション全体を通じてコンテンツが表示され続けます。
  • スタンドアロンのtransition-behavior: allow-discreteを使う場合、transitionショートハンドのに記述しなければブラウザに無視されます。
  • dialogやpopoverではoverlay ... allow-discreteもトランジションに含める必要がありますが、overlayはまだBaselineではないためプログレッシブエンハンスメントとして扱ってください。
  • @starting-styleallow-discreteは、2024年8月6日にFirefox 129がリリースされてからBaseline Newly availableとなっています。サポートされていない環境では、要素は即座に表示・非表示になります。

なぜdisplay: noneをトランジションできなかったのか

CSSトランジションは、アニメーションの起点となる「変更前のスタイル」を必要とします。display: noneの状態にある要素や、DOMに挿入されたばかりの要素にはレンダリング済みの前の状態がないため、従来はブラウザが補間の基準を持てず、トランジションが発火しませんでした。これが開発者たちがsetTimeoutによるクラス切り替えや、二重のrequestAnimationFrameを使ったリフロー回避策に頼ってきた理由です。

よく使われる代替手段——opacity: 0でフェードさせて要素をDOMに残す方法——は本質的な解決策ではありません。opacity: 0にしても要素はレイアウト上に残り、タブ順序にも、アクセシビリティツリーにも存在し続けるため、キーボードユーザーやスクリーンリーダーユーザーは、見た目上は消えているコンテンツに到達できてしまいます。display: noneはこれら3つすべてから要素を取り除きます。実際、移行が途中のコンポーネントのセッションリプレイを見ると、まさにこの種のバグが浮き彫りになります。閉じたように見えるパネルに、ユーザーがタブキーやクリックで到達できてしまうのは、フェードアウトされただけでレイアウトから取り除かれていないからです。実際のdisplay: noneをアニメーションさせることで、このバグを根本から解消できます。

問題を解決する2つのCSS機能:allow-discrete@starting-style

display離散プロパティです。値を補間することができず、オン・オフのスイッチのように機能します。離散プロパティのアニメーションは通常、2つの値の間でアニメーション中間点(50%)で切り替わります。ただし、display: noneまたはcontent-visibility: hiddenとの間でアニメーションする場合は例外で、ブラウザはトランジション中のコンテンツがアニメーション全体を通じて表示されるよう値を切り替えます。方向も重要です。displaynoneからblockへアニメーションする場合、値はアニメーション開始0%のタイミングでblockに切り替わり、全体を通じて表示されます。blockからnoneへアニメーションする場合は、100%のタイミングでnoneに切り替わるため、やはり全体を通じて表示されます——これが両方向でフェードが実際に見える状態を維持する仕組みです。

これを有効にするには、displayのトランジションにtransition-behavior: allow-discreteを設定し、@starting-styleでエントリー状態を定義します。

エグジットのレシピ:display: noneへのトランジション

要素をフェードアウトさせてからレイアウトから取り除くには、transitionallow-discrete付きでdisplayを含め、非表示状態を設定します。

.panel {
  opacity: 1;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.panel.is-hidden {
  opacity: 0;
  display: none;
}

opacityは300msかけて0へアニメーションし、displayは100%になるまで表示状態を維持してからnoneに切り替わります。displayへのallow-discreteを忘れると要素が即座に消えてしまいます——このテクニックで最もよくあるミスです。

エントリーのレシピ:display: noneからのアニメーション

非表示状態から始まる要素には、「開く前」の値を@starting-styleルールに記述します。CSSネストを使えば、すべてを1つのブロックにまとめられます。

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

.panel.is-open {
  display: block;
  opacity: 1;

  @starting-style {
    opacity: 0;
  }
}

順序が重要です。@starting-styleはターゲットとするルールと同じ詳細度を持つため、カスケードで優先されるよう、open状態の宣言の後に記述する必要があります。また、1行の落とし穴があります。transition-behavior: allow-discretetransitionショートハンドより前に記述すると、ブラウザはtransition-behaviorを無視します。スタンドアロンで書く場合は最後に置きます。

.panel {
  transition: opacity 0.3s, display 0.3s;
  transition-behavior: allow-discrete; /* ショートハンドの後に記述すること */
}

古いブラウザエンジン向けに、MDNのクロスブラウザパターンではtransitionを2回宣言します。最初の宣言はallow-discreteなしで記述することで、transition-behaviorをサポートしないブラウザでも他のプロパティのトランジションが機能するクロスブラウザサポートを確保します。

dialogとpopover:overlayの落とし穴

トップレイヤー要素——<dialog>popover属性を使用するすべての要素——は最も重要なユースケースであり、追加の要件があります。dialogやpopoverではoverlay ... allow-discreteもトランジションに含める必要があります。そうしないと要素がトップレイヤーから即座に外れ、エグジットアニメーションが表示されません。

dialog {
  translate: 0 100vh;
  transition:
    translate 0.4s ease-out,
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

dialog[open] {
  translate: 0 0;

  @starting-style {
    translate: 0 100vh;
  }
}

popoverの場合はdialog[open]:popover-open疑似クラスに置き換えます。overlayプロパティがトップレイヤーからの離脱を遅延させる役割を担います。アニメーションが完了するまでトップレイヤーからの要素の削除が延期されるよう保証します。より複雑なケースでは、これを行わないとオーバーレイから要素が早すぎるタイミングで削除され、アニメーションがスムーズかつ効果的に機能しなくなります。

正直に言っておくべき注意点があります。overlayはBaselineではありません。MDNは実験的な機能としてマークしており、広く使われているブラウザの一部では動作しないため、Baselineには含まれていません。プログレッシブエンハンスメントとして扱ってください。サポートされていない環境でも、dialogは引き続き開閉しますが、エグジットの遅延処理は行われません。

ブラウザサポートとグレースフルデグラデーション

@starting-styletransition-behavior: allow-discreteはどちらも、2024年8月6日にリリースされたFirefox 129によってBaseline Newly availableとなりました。ただし、display自体のアニメーションにはこれら2つの機能のリリース以上のものが必要です。Chrome・Edge 117以降とSafari 18以降で動作します。Safari 17.4ではtransition-behaviorが、17.5では@starting-styleが追加されましたが、displayのトランジションはSafari 18からのみ機能します。Firefox 129以降は両機能をサポートしていますが、2026年半ば時点ではdisplayプロパティ自体のトランジションはまだサポートされておらず、Firefoxでは要素が即座に表示・非表示になります。@keyframes内でのdisplayはChrome 116以降で動作します。

機能ステータスフォールバック
transition-behavior: allow-discreteBaseline(2024年8月)即時表示・非表示
@starting-styleBaseline(2024年8月)エントリーアニメーションなし
overlayBaseline未対応dialogは引き続き開閉

これは純粋なプログレッシブエンハンスメントです。これらの機能がない場合、トップレイヤーへのアニメーションやdisplay: noneスタイルからのアニメーションは、現在と同様にトランジションなしで単純に表示されます。ポリフィルもJavaScriptのフォールバックも不要です。明示的な境界が必要な場合は、機能検出でエンハンスメントをゲートできます。

@supports (transition-behavior: allow-discrete) {
  /* モダンなエントリー・エグジットアニメーション */
}

View Transitions APIを使うべき場面

これらのトランジションは、既存の要素の表示・非表示を切り替える場合に使用します。DOMノードを追加・削除する場合は、代わりにView Transitions APIを使用してください。同一ドキュメントのビュートランジションは、2025年10月14日に同日リリースされたFirefox 144をもってBaseline Newly availableとなり、Chrome 111以降、Edge 111以降、Safari 18以降、Firefox 144以降でサポートされています。DOM操作をdocument.startViewTransition()でラップし、未サポートのブラウザ向けにシンプルなフォールバックを用意します。

if (document.startViewTransition) {
  document.startViewTransition(() => card.remove());
} else {
  card.remove();
}

モダンなレシピは旧来のJavaScriptによる制御を不要にします。transitionallow-discrete付きでdisplayを含め、@starting-styleにエントリー状態を定義し、トップレイヤー要素にはoverlayを追加し、未サポートのブラウザでは即時切り替えにフォールバックさせましょう。setTimeoutを捨てて、CSSをリリースしてください。

よくある質問

`display`のトランジションに`allow-discrete`を追加してもエグジットアニメーションが発火しないのはなぜですか?

最も一般的な原因は、スタンドアロンの`transition-behavior: allow-discrete`の宣言が`transition`ショートハンドより前に記述されており、ブラウザが暗黙的にそれを無視していることです。別プロパティとして記述する場合、`transition-behavior`は`transition`ショートハンドの後に記述する必要があります。そうしないとショートハンドによってリセットされます。`allow-discrete`を`transition`の値の中にインラインで記述する場合は、ショートハンド内の順序は問題なく、この落とし穴は適用されません。

要素を`display: none`にフェードアウトさせるだけなら`@starting-style`は必要ですか?

必要ありません。`@starting-style`はエントリーアニメーション——要素が`display: none`の状態から、またはDOMに新たに挿入されてアニメーションの起点となる「開く前」の状態が必要な場合——にのみ必要です。既に表示されている要素を`display: none`へアニメーションする純粋なエグジットには、`allow-discrete`付きで`display`をトランジションに含め、非表示状態を設定するだけで十分です。`@starting-style`は要素のエントリーアニメーションも行う場合にのみ追加してください。

`transition-behavior`や`@starting-style`をサポートしていないブラウザではどうなりますか?

要素はアニメーションなしで即座に表示・非表示になります。これはプログレッシブエンハンスメントであるため、ポリフィルもJavaScriptのフォールバックも不要です。dialogやpopoverにおいて、Baseline未対応の`overlay`プロパティをサポートしていないブラウザでも、要素の開閉は正常に機能します。ただし、トップレイヤーからの遅延エグジットはスキップされます。`@supports (transition-behavior: allow-discrete)`ルールを使って、エンハンスメントを明示的にゲートすることもできます。

いつView Transitions APIを使い、いつ`display: none`のアニメーションを使うべきですか?

DOMノードを追加・削除する場合はView Transitionsを使用し、すでにDOMに存在する要素の表示・非表示を切り替える場合はdisplayトランジションを使用してください。同一ドキュメントのビュートランジションは2025年10月14日にBaseline Newly availableとなり、Chrome 111以降、Edge 111以降、Safari 18以降、Firefox 144以降でサポートされています。DOM操作を`document.startViewTransition()`でラップし、未サポートのブラウザ向けにシンプルなフォールバックを用意してください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.