display: noneからふわっと表示する。CSSだけで書ける@starting-style

opacity: 0から1ならトランジションできるのに、display: noneだった要素を表示した瞬間はふわっと出てくれない。表示・非表示のUIで、ここにJavaScriptを足した経験があるかもしれません。

今は@starting-styletransition-behavior: allow-discreteを組み合わせると、CSSトランジションだけで扱える範囲が広がります。どちらも2024年8月からBaseline Newly availableです。

特にポップオーバーや<dialog>の開閉に使いやすい組み合わせです。

なぜdisplay: noneから普通のtransitionが始まらなかったのか

CSSトランジションは、変更前と変更後のスタイルがあって、その間を補間するときに動きます。

ところが、ページ上に初めて表示される要素や、display: noneから表示へ変わった要素には、通常の意味で「前の描画状態」がありません。そのため、最初の表示ではトランジションが開始されませんでした。

@starting-styleは、その「前の状態」をCSS側で用意するためのルールです。

.notice {
  opacity: 1;
  transition: opacity 0.25s;

  @starting-style {
    opacity: 0;
  }
}

この要素が初めて表示されるとき、opacity: 0を開始値として1までトランジションできます。

display自体を切り替えるにはallow-discreteも使う

displayは、数値のように途中の値を補間するプロパティではありません。このような離散的なプロパティをトランジションへ含めるために使うのがtransition-behavior: allow-discreteです。

MDNの例を単純化すると、次のような組み合わせになります。

.card {
  display: block;
  opacity: 1;
  transition:
    opacity 0.25s,
    display 0.25s allow-discrete;

  @starting-style {
    opacity: 0;
  }
}

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

ここで@starting-styleが担当するのは、表示される瞬間の開始スタイルです。allow-discreteは、displayのような離散プロパティをトランジション対象として扱えるようにします。

この例の.is-hiddenを付け外しする仕組み自体は別途必要です。HTMLだけで開閉できるPopover APIを使うと、この部分もJavaScriptなしで試せます。

popoverなら開閉もHTMLで書ける

ボタンのpopovertargetでポップオーバーを開閉し、見た目の変化をCSSへ任せます。ボタン側はHTMLのcommand属性でも書けるので、<dialog>と操作の書き方をそろえたいときはそちらも使えます。

<button popovertarget="help">ヘルプを開く</button>
<div id="help" popover>ここに補足を表示します。</div>

CSSでは、閉じた状態を通常ルール、開いた状態を:popover-openで指定します。

[popover] {
  opacity: 0;
  transform: translateY(0.5rem);
  transition:
    opacity 0.25s,
    transform 0.25s,
    display 0.25s allow-discrete,
    overlay 0.25s allow-discrete;
}

[popover]:popover-open {
  opacity: 1;
  transform: translateY(0);
}

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

overlayをトランジションへ入れるのは、ポップオーバーが閉じるときに、アニメーションが終わるまでtop layerから取り除かれるのを遅らせるためです。displayと同じくallow-discreteを付けます。

これで、開くときだけでなく閉じるときもフェードと移動を見せられます。

dialogも同じ考え方で動かせる

<dialog>も、開いている状態のdialog:open、初回表示の@starting-styledisplayoverlayの離散トランジションを組み合わせられます。

仕組みはポップオーバーと同じです。

  • 表示中の最終状態を書く
  • 非表示側の状態を書く
  • @starting-styleで表示直前の開始値を書く
  • displayoverlayallow-discreteを付ける

@starting-styleだけ追加しても、閉じるアニメーションまで自動で完成するわけではありません。top layerから外れるタイミングまで含めてtransitionを組むのがポイントです。

対応状況は、機能ごとに分けて見る

@starting-styletransition-behaviorは、どちらも2024年8月にBaseline Newly availableです。ただし、displayのトランジションやoverlayには別の対応状況があります。

2026年9月15日に確認した対応開始は次のとおりです。

機能Chrome・EdgeFirefoxSafari
@starting-style117以降129以降17.5以降
transition-behavior本体117以降129以降17.4以降
displayの離散トランジション117以降未対応18以降
overlay117以降未対応未対応

つまり、開くときのフェードは広く動く一方、閉じるときの見え方はブラウザによって変わります。この記事のコードも、開くアニメーションを基本にして、閉じる側は対応がある環境での追加と考えます。未対応なら、一瞬で閉じる動きが残ります。

すべてのブラウザで同じ見た目を必須にするのか、開閉そのものが使えればよいのか。ここを決めておくと、必要な実装量も変わります。

動きを減らす設定ではトランジションを止める

フェードだけなら動きは小さめですが、拡大・移動も組み合わせる場合はprefers-reduced-motionを確認します。

@media (prefers-reduced-motion: reduce) {
  [popover],
  dialog {
    transition: none;
  }
}

表示・非表示そのものは残し、演出だけを止めます。

うまく動かないときは、指定順と最終状態を見る

@starting-styleにも、通常のCSSと同じカスケードが働きます。同じ強さのセレクタなら、通常状態のルールより後に置くことで、開始値が上書きされないようにします。詳細度の高い別ルールや!importantがあれば、記述順だけでは解決しません。

確認するときは、次の操作を順に試します。

  1. ボタンで開いて、閉じる
  2. Escや外側の操作で閉じる
  3. すぐに開き直す
  4. 動きを減らす設定で使う

アニメーションの途中だけでなく、最後に内容が読めるか、閉じた要素が操作の邪魔をしないかも見ます。

「表示された瞬間だけ動かない」をCSSで埋められる

@starting-styleの役割はかなり限定的です。状態管理をする機能ではなく、「前の描画状態がない要素に、トランジション開始値を与える」ためのCSSです。

そこへallow-discreteを組み合わせると、これまでdisplay: noneが壁になっていたUIにもトランジションを付けやすくなります。

表示のためだけにタイマーやクラス付与のJavaScriptを書いていた箇所があれば、ポップオーバーやダイアログから試すと違いを確認しやすいです。

SNS SHARE

はてなブックマークでシェアXでシェアLINEでシェアPocketでシェア