CSS @starting-style — display: noneからのアニメーションがついにJSなしで可能に
モーダルを開く時、トーストを表示する時、ドロップダウンを展開する時。display: noneから要素を表示する場面は日常的にあります。しかしCSSのtransitionはdisplay: none → blockの変化に対して効きません。そのため多くの開発者がJavaScriptでタイミングを制御したり、max-heightハックに頼ったりしてきました。
CSS @starting-style はこの制約を根本から解消します。全主要ブラウザで対応済み(Baseline 2024)。
Before / After の比較デモを用意しました。 実際に触って違いを体感してみてください。
何が問題だったのか
CSSのdisplayプロパティは**離散値(discrete value)**です。opacityのように0から1へ補間できるプロパティとは異なり、noneとblockの間に中間状態がありません。
/* これは動かない */
.modal {
display: none;
opacity: 0;
transition: opacity 0.3s, display 0.3s;
}
.modal.visible {
display: block;
opacity: 1;
}
display: noneの要素はレンダリングツリーから完全に除外されるため、display: blockに変わった瞬間、ブラウザはトランジションの「開始値」を知りません。結果として、要素はアニメーションなしにパッと表示されます。
これまでの回避策
max-heightハック:
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s;
}
.accordion-content.open {
max-height: 500px; /* 十分大きい値 */
}
実際の高さが80pxなのに500pxまでアニメーションするため、見た目上のアニメーションが最初の16%で完了し、残りの時間は何も変化しない「空振り」区間になります。テキスト量によってはmax-heightが足りずにコンテンツが途切れることもあります。
JSタイミングハック:
modal.style.display = 'block';
// 1フレーム待ってからクラスを追加
requestAnimationFrame(() => {
requestAnimationFrame(() => {
modal.classList.add('visible');
});
});
display: blockの適用後にブラウザがレイアウトを計算するのを待ってから、opacityやtransformを変更する。2重のrequestAnimationFrameやsetTimeout(0)が必要で、タイミングが不安定になることがあります。
@starting-styleの仕組み
@starting-styleは、要素がDOMに追加された時やdisplay: noneから表示状態に変わった時に、トランジションの開始状態を定義するat-ruleです。
.modal {
/* 表示状態(= トランジションのゴール) */
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.35s ease,
transform 0.35s ease,
display 0.35s allow-discrete;
/* 入場アニメーションの開始状態 */
@starting-style {
opacity: 0;
transform: translateY(24px) scale(0.94);
}
}
/* 退場アニメーションの終了状態 */
.modal.hidden {
display: none;
opacity: 0;
transform: translateY(24px) scale(0.94);
}
ポイントは3つです。
1. @starting-styleで「どこから始めるか」を定義
@starting-styleブロック内のスタイルは、要素が初めて表示される時のみ適用されます。以降のトランジションではメインルールのスタイルが開始値になるため、入場と退場で異なるアニメーションを定義できます。
2. transition-behavior: allow-discreteが必須
displayのような離散値プロパティをトランジションに参加させるには、transition-behavior: allow-discreteが必要です。ショートハンドでも書けます。
/* ショートハンド */
transition: display 0.35s allow-discrete;
/* ロングハンド */
transition-property: display;
transition-duration: 0.35s;
transition-behavior: allow-discrete;
3. overlayプロパティでトップレイヤーを制御
<dialog>やpopover要素はブラウザのトップレイヤーに配置されます。閉じる時にアニメーションが完了する前にトップレイヤーから削除されてしまうと、アニメーションが中断されます。overlayプロパティをトランジションに含めることで、アニメーション完了まで要素をトップレイヤーに維持できます。
dialog {
transition:
opacity 0.35s,
transform 0.35s,
overlay 0.35s allow-discrete,
display 0.35s allow-discrete;
}
実装パターン
モーダル(dialog要素)
<dialog>要素のshowModal()は自動的にバックドロップを生成し、中央配置してくれます。@starting-styleと組み合わせれば、アニメーションのJSコードはゼロです。
dialog {
opacity: 1;
transform: translateY(0) scale(1);
margin: auto;
transition:
opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
overlay 0.35s allow-discrete,
display 0.35s allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(24px) scale(0.94);
}
}
/* 閉じる時の状態 */
dialog:not([open]) {
opacity: 0;
transform: translateY(24px) scale(0.94);
}
/* バックドロップもアニメーション */
dialog::backdrop {
background: rgba(0, 0, 0, 0);
transition:
background 0.35s,
overlay 0.35s allow-discrete,
display 0.35s allow-discrete;
}
dialog[open]::backdrop {
background: rgba(0, 0, 0, 0.6);
}
@starting-style {
dialog[open]::backdrop {
background: rgba(0, 0, 0, 0);
}
}
// JSはopen/closeの制御のみ
dialog.showModal();
dialog.close();
トースト通知
右からスライドインして、消える時もスライドアウトします。
.toast {
display: none;
opacity: 1;
transform: translateX(0);
transition:
opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
display 0.4s allow-discrete;
@starting-style {
opacity: 0;
transform: translateX(110%);
}
}
.toast.visible {
display: block;
}
.toast.removing {
opacity: 0;
transform: translateX(110%);
}
退場アニメーションでは.removingクラスを追加してopacity: 0にした後、トランジション完了後に.visibleを外します。
ドロップダウンメニュー
上から降りてくる自然なアニメーションです。
.dropdown {
display: none;
opacity: 1;
transform: translateY(0);
transition:
opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
display 0.2s allow-discrete;
@starting-style {
opacity: 0;
transform: translateY(-8px);
}
}
.dropdown.visible {
display: block;
}
ブラウザサポート
| ブラウザ | 対応バージョン |
|---|---|
| Chrome | 117+(2023年9月〜) |
| Edge | 117+ |
| Safari | 17.5+(2024年5月〜) |
| Firefox | 129+(2024年8月〜) |
2024年8月のFirefox対応をもって、Baseline Newly Availableに到達しています。主要ブラウザ全てで利用可能です。
@starting-styleを使うべき場面・使わなくていい場面
使うべき場面:
display: noneから表示する要素(モーダル、ドロップダウン、トースト、ツールチップ)<dialog>要素の開閉popover属性を使ったポップオーバー- DOMに動的に追加される要素のエントリーアニメーション
使わなくていい場面:
- 最初から表示されている要素のアニメーション(
@starting-styleなしでtransitionだけで十分) visibility: hiddenからのアニメーション(visibilityは元々トランジション可能)@keyframesアニメーション(@starting-styleはトランジション専用)
まとめ
@starting-styleは「モーダルの開閉アニメーション」という日常的な実装を、JSのタイミングハックから解放するCSSの新機能です。transition-behavior: allow-discreteと組み合わせることで、display: noneからの滑らかなトランジションが数行のCSSで実現できます。