CSS離散アニメーション実践ガイド: display/visibilityをアニメーション化
CSS離散プロパティ(discrete properties)のアニメーション化は、長年の課題でした。しかしtransition-behavior: allow-discreteの登場により、displayやvisibilityをスムーズに遷移できるようになりました。
離散アニメーションとは
従来、CSSでアニメーション可能だったのは連続値(opacity: 0~1など)のみでした。離散値(display: none/block)は中間状態が存在しないため、瞬時に切り替わっていました。
従来の問題
/* これではアニメーションしない */
.modal {
display: none;
opacity: 0;
transition: opacity 0.3s;
}
.modal.open {
display: block;
opacity: 1;
}
display: noneからblockへは即座に切り替わるため、opacityのトランジションが発火しません。
transition-behavior: allow-discrete
この新しいプロパティで、離散値のアニメーションが可能になります。
基本構文
.element {
transition-property: display, opacity;
transition-duration: 0.3s;
transition-behavior: allow-discrete;
}
display プロパティのアニメーション
最も実用的なのが、モーダルやドロップダウンのフェードイン/アウトです。
フェードインモーダル
.modal {
display: none;
opacity: 0;
transition:
opacity 0.3s ease-out,
display 0.3s allow-discrete;
}
.modal.open {
display: block;
opacity: 1;
}
/* transitionプロパティの短縮記法 */
.modal-short {
display: none;
opacity: 0;
transition:
opacity 0.3s,
display 0.3s allow-discrete;
}
.modal-short.open {
display: block;
opacity: 1;
}
スライドダウンメニュー
.dropdown-menu {
display: none;
opacity: 0;
transform: translateY(-10px);
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out,
display 0.25s allow-discrete;
}
.dropdown-menu.open {
display: block;
opacity: 1;
transform: translateY(0);
}
ツールチップ
.tooltip {
display: none;
opacity: 0;
scale: 0.95;
transition:
opacity 0.2s,
scale 0.2s,
display 0.2s allow-discrete;
}
.tooltip-trigger:hover .tooltip {
display: block;
opacity: 1;
scale: 1;
}
visibility プロパティのアニメーション
visibilityも同様にアニメーション可能です。displayとの違いは、レイアウト空間を保持する点です。
基本例
.element {
visibility: hidden;
opacity: 0;
transition:
opacity 0.3s,
visibility 0.3s allow-discrete;
}
.element.visible {
visibility: visible;
opacity: 1;
}
タブコンテンツ切り替え
.tab-content {
visibility: hidden;
opacity: 0;
position: absolute;
transition:
opacity 0.3s ease-in-out,
visibility 0.3s allow-discrete;
}
.tab-content.active {
visibility: visible;
opacity: 1;
position: relative;
}
@starting-style の活用
@starting-styleルールで、初期レンダリング時のアニメーションを制御できます。
ページロード時のフェードイン
.hero {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.5s ease-out,
transform 0.5s ease-out;
}
@starting-style {
.hero {
opacity: 0;
transform: translateY(20px);
}
}
displayと組み合わせる
dialog {
opacity: 1;
scale: 1;
transition:
opacity 0.3s,
scale 0.3s,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
@starting-style {
dialog[open] {
opacity: 0;
scale: 0.9;
}
}
dialog:not([open]) {
opacity: 0;
scale: 0.9;
}
実践例: モーダルダイアログ
完全に機能するモーダルの実装例です。
HTML
<button id="openModal">モーダルを開く</button>
<dialog id="modal">
<div class="modal-content">
<h2>モーダルタイトル</h2>
<p>モーダルの内容がここに入ります。</p>
<button id="closeModal">閉じる</button>
</div>
</dialog>
CSS
dialog {
border: none;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
opacity: 0;
scale: 0.95;
transition:
opacity 0.3s ease-out,
scale 0.3s ease-out,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
dialog[open] {
opacity: 1;
scale: 1;
}
@starting-style {
dialog[open] {
opacity: 0;
scale: 0.95;
}
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0);
transition: background-color 0.3s;
}
dialog[open]::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
JavaScript
const modal = document.getElementById('modal')
const openBtn = document.getElementById('openModal')
const closeBtn = document.getElementById('closeModal')
openBtn.addEventListener('click', () => {
modal.showModal()
})
closeBtn.addEventListener('click', () => {
modal.close()
})
// 背景クリックで閉じる
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.close()
}
})
実践例: アコーディオンメニュー
<div class="accordion">
<button class="accordion-trigger">セクション1</button>
<div class="accordion-content">
<p>アコーディオンの内容がここに表示されます。</p>
</div>
</div>
.accordion-content {
display: none;
opacity: 0;
max-height: 0;
overflow: hidden;
transition:
opacity 0.3s ease-out,
max-height 0.3s ease-out,
display 0.3s allow-discrete;
}
.accordion.open .accordion-content {
display: block;
opacity: 1;
max-height: 500px;
}
document.querySelectorAll('.accordion-trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
trigger.parentElement.classList.toggle('open')
})
})
パフォーマンス最適化
will-change の活用
.modal {
will-change: opacity, transform;
transition:
opacity 0.3s,
transform 0.3s,
display 0.3s allow-discrete;
}
/* アニメーション完了後にwill-changeをクリア */
.modal.animation-complete {
will-change: auto;
}
ハードウェアアクセラレーション
.animated-element {
/* transformやopacityはGPUアクセラレーション可能 */
transform: translateZ(0);
transition:
opacity 0.3s,
transform 0.3s,
display 0.3s allow-discrete;
}
アクセシビリティへの配慮
ARIA属性との連携
<button
aria-expanded="false"
aria-controls="dropdown"
id="dropdown-trigger">
メニュー
</button>
<div
id="dropdown"
role="menu"
aria-labelledby="dropdown-trigger"
class="dropdown-menu">
<!-- メニュー項目 -->
</div>
const trigger = document.getElementById('dropdown-trigger')
const dropdown = document.getElementById('dropdown')
trigger.addEventListener('click', () => {
const isOpen = dropdown.classList.contains('open')
dropdown.classList.toggle('open')
trigger.setAttribute('aria-expanded', !isOpen)
})
キーボード操作対応
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.hasAttribute('open')) {
modal.close()
}
})
ブラウザサポート
transition-behavior: allow-discreteは比較的新しい機能です。
サポート状況(2025年2月時点):
- Chrome 117+
- Edge 117+
- Safari 17.4+
- Firefox 129+
フォールバック
/* 非対応ブラウザ向けフォールバック */
.modal {
display: none;
opacity: 0;
}
.modal.open {
display: block;
animation: fadeIn 0.3s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 対応ブラウザではtransitionを使用 */
@supports (transition-behavior: allow-discrete) {
.modal {
animation: none;
transition:
opacity 0.3s,
transform 0.3s,
display 0.3s allow-discrete;
}
.modal.open {
opacity: 1;
transform: scale(1);
}
}
まとめ
transition-behavior: allow-discreteにより、CSSアニメーションの可能性が大きく広がりました。
メリット:
- JavaScriptなしでスムーズなUI遷移が可能
- コード量の削減
- パフォーマンスの向上
- メンテナンス性の改善
適したユースケース:
- モーダル・ダイアログ
- ドロップダウンメニュー
- ツールチップ
- アコーディオン
- タブコンテンツ
JavaScriptでのクラス切り替えと組み合わせることで、モダンでアクセシブルなUIコンポーネントを簡単に実装できます。