CSS interpolate-sizeでアニメーション可能な高さ - auto値のスムーズな遷移を実現
CSS interpolate-sizeでアニメーション可能な高さ
Web開発において、height: autoやwidth: autoをアニメーションさせることは長年の課題でした。CSSの新しいプロパティinterpolate-sizeは、この問題を解決し、コンテンツサイズに応じた自動調整とスムーズなアニメーションを両立させます。
従来の課題
height: autoがアニメーションできない問題
/* これは動作しない */
.accordion-content {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.accordion-content.open {
height: auto; /* autoへの遷移はアニメーションされない */
}
これまでの回避策とその問題点
1. max-heightトリック
/* 回避策1: max-heightを使う */
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion-content.open {
max-height: 1000px; /* 十分大きな値を設定 */
}
問題点:
- コンテンツの実際の高さがわからない
- max-heightを大きくしすぎるとアニメーションが不自然
- コンテンツが予想より大きい場合に切れる
2. JavaScriptで高さを計算
// 回避策2: JavaScriptで実際の高さを取得
const content = document.querySelector('.accordion-content');
const height = content.scrollHeight;
content.style.height = `${height}px`;
問題点:
- JavaScriptが必須
- リサイズ時の再計算が必要
- パフォーマンスへの影響
- コンテンツが動的に変化する場合の対応が複雑
3. transform: scaleY()
/* 回避策3: scaleYを使う */
.accordion-content {
transform: scaleY(0);
transform-origin: top;
transition: transform 0.3s ease;
}
.accordion-content.open {
transform: scaleY(1);
}
問題点:
- 内部のテキストも縦方向に圧縮される
- 見た目が不自然
- レイアウトへの影響
interpolate-sizeの登場
CSS Working Groupによって提案されたinterpolate-sizeプロパティは、この問題を根本的に解決します。
基本構文
.element {
interpolate-size: allow-keywords;
}
ブラウザサポート
/* ブラウザサポートチェック */
@supports (interpolate-size: allow-keywords) {
/* interpolate-sizeをサポートするブラウザ向けのスタイル */
}
現在のサポート状況(2025年2月時点):
- Chrome/Edge 123+
- Safari 18+(実験的機能)
- Firefox: 開発中
基本的な使い方
アコーディオンの実装
<div class="accordion">
<button class="accordion-trigger">クリックして展開</button>
<div class="accordion-content">
<p>これはアコーディオンのコンテンツです。</p>
<p>任意の長さのコンテンツを含むことができます。</p>
</div>
</div>
/* interpolate-sizeを有効化 */
.accordion-content {
interpolate-size: allow-keywords;
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
/* 開いた状態 */
.accordion-content.open {
height: auto; /* これでアニメーションが動作する! */
}
// JavaScriptでトグル
document.querySelector('.accordion-trigger').addEventListener('click', () => {
document.querySelector('.accordion-content').classList.toggle('open');
});
横方向のアニメーション
.sidebar {
interpolate-size: allow-keywords;
width: 0;
overflow: hidden;
transition: width 0.4s ease-in-out;
}
.sidebar.expanded {
width: auto; /* コンテンツ幅に応じて自動調整 */
}
実践的な例
カード展開UI
<div class="card">
<div class="card-header">
<h3>製品情報</h3>
<button class="expand-btn">詳細を見る</button>
</div>
<div class="card-details">
<p>製品の詳細説明がここに入ります。</p>
<ul>
<li>特徴1</li>
<li>特徴2</li>
<li>特徴3</li>
</ul>
</div>
</div>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
.card-details {
interpolate-size: allow-keywords;
height: 0;
overflow: hidden;
opacity: 0;
transition:
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease;
}
.card.expanded .card-details {
height: auto;
opacity: 1;
}
/* スムーズなイージング関数 */
.card-details {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
ドロップダウンメニュー
<nav class="dropdown">
<button class="dropdown-trigger">メニュー</button>
<ul class="dropdown-menu">
<li><a href="/home">ホーム</a></li>
<li><a href="/about">会社概要</a></li>
<li><a href="/services">サービス</a></li>
<li><a href="/contact">お問い合わせ</a></li>
</ul>
</nav>
.dropdown {
position: relative;
}
.dropdown-menu {
interpolate-size: allow-keywords;
position: absolute;
top: 100%;
left: 0;
height: 0;
overflow: hidden;
background: white;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition:
height 0.25s ease-out,
box-shadow 0.25s ease-out;
list-style: none;
padding: 0;
margin: 0;
}
.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu {
height: auto;
}
.dropdown-menu li {
padding: 0.75rem 1rem;
}
.dropdown-menu a {
color: inherit;
text-decoration: none;
display: block;
}
.dropdown-menu a:hover {
background: #f5f5f5;
}
モーダルのコンテンツエリア
<div class="modal">
<div class="modal-header">
<h2>タイトル</h2>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>動的な長さのコンテンツ</p>
</div>
<div class="modal-footer">
<button>キャンセル</button>
<button>OK</button>
</div>
</div>
.modal-body {
interpolate-size: allow-keywords;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.modal.open .modal-body {
max-height: 80vh; /* ビューポート高さに応じて調整 */
}
/* スクロール可能に */
.modal-body {
overflow-y: auto;
}
アニメーションのカスタマイズ
イージング関数
/* 標準的なイージング */
.smooth-open {
interpolate-size: allow-keywords;
transition: height 0.3s ease-in-out;
}
/* カスタムベジェ曲線 */
.bouncy {
interpolate-size: allow-keywords;
transition: height 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* 段階的なアニメーション */
.stepped {
interpolate-size: allow-keywords;
transition: height 0.4s steps(5);
}
遅延とディレイ
.staggered-items > * {
interpolate-size: allow-keywords;
transition: height 0.3s ease;
}
.staggered-items > *:nth-child(1) { transition-delay: 0s; }
.staggered-items > *:nth-child(2) { transition-delay: 0.1s; }
.staggered-items > *:nth-child(3) { transition-delay: 0.2s; }
.staggered-items > *:nth-child(4) { transition-delay: 0.3s; }
複合プロパティのアニメーション
.complex-animation {
interpolate-size: allow-keywords;
height: 0;
opacity: 0;
transform: translateY(-20px);
transition:
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease,
transform 0.3s ease;
}
.complex-animation.visible {
height: auto;
opacity: 1;
transform: translateY(0);
}
プログレッシブエンハンスメント
フォールバック戦略
/* 基本スタイル(全ブラウザ) */
.expandable {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.expandable.open {
max-height: 500px; /* フォールバック */
}
/* interpolate-sizeサポート時の最適化 */
@supports (interpolate-size: allow-keywords) {
.expandable {
interpolate-size: allow-keywords;
height: 0;
max-height: none; /* max-heightの制限を解除 */
transition: height 0.3s ease;
}
.expandable.open {
height: auto;
}
}
JavaScriptによる機能検出
// interpolate-sizeのサポートを確認
function supportsInterpolateSize() {
return CSS.supports('interpolate-size', 'allow-keywords');
}
// サポート状況に応じて処理を分岐
if (supportsInterpolateSize()) {
// interpolate-sizeを使用
element.style.interpolateSize = 'allow-keywords';
element.style.height = 'auto';
} else {
// フォールバック: JavaScriptで高さを計算
const height = element.scrollHeight;
element.style.height = `${height}px`;
}
パフォーマンス最適化
will-changeの活用
.accordion-content {
interpolate-size: allow-keywords;
will-change: height;
transition: height 0.3s ease;
}
/* アニメーション完了後にwill-changeを解除 */
.accordion-content.animating {
will-change: height;
}
element.addEventListener('transitionend', () => {
element.classList.remove('animating');
});
contain プロパティ
.expandable {
interpolate-size: allow-keywords;
contain: layout style; /* レイアウト計算を最適化 */
transition: height 0.3s ease;
}
content-visibilityとの組み合わせ
.lazy-expand {
interpolate-size: allow-keywords;
content-visibility: auto; /* オフスクリーン時のレンダリングをスキップ */
contain-intrinsic-size: 0 500px; /* 概算サイズを指定 */
transition: height 0.3s ease;
}
アクセシビリティ
ARIAラベルの適切な使用
<button
class="accordion-trigger"
aria-expanded="false"
aria-controls="accordion-content-1">
セクション1
</button>
<div
id="accordion-content-1"
class="accordion-content"
role="region"
aria-labelledby="accordion-trigger">
<!-- コンテンツ -->
</div>
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
trigger.setAttribute('aria-expanded', !isExpanded);
content.classList.toggle('open');
});
prefers-reduced-motionへの対応
.expandable {
interpolate-size: allow-keywords;
transition: height 0.3s ease;
}
/* アニメーションを減らす設定のユーザー向け */
@media (prefers-reduced-motion: reduce) {
.expandable {
transition-duration: 0.01ms;
}
}
キーボードナビゲーション
/* フォーカス時の視覚的フィードバック */
.accordion-trigger:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* フォーカス時にコンテンツも表示 */
.accordion-trigger:focus-within + .accordion-content {
height: auto;
}
実装パターン集
ネストされたアコーディオン
<div class="accordion-group">
<div class="accordion-item">
<button class="accordion-trigger">親項目1</button>
<div class="accordion-content">
<div class="accordion-item nested">
<button class="accordion-trigger">子項目1-1</button>
<div class="accordion-content">
コンテンツ
</div>
</div>
</div>
</div>
</div>
.accordion-content {
interpolate-size: allow-keywords;
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.accordion-content.open {
height: auto;
}
/* ネストレベルに応じて遅延 */
.nested .accordion-content {
transition-delay: 0.1s;
}
タブパネル
<div class="tabs">
<div role="tablist">
<button role="tab" aria-selected="true">タブ1</button>
<button role="tab">タブ2</button>
<button role="tab">タブ3</button>
</div>
<div role="tabpanel" class="tab-panel active">
タブ1のコンテンツ
</div>
<div role="tabpanel" class="tab-panel">
タブ2のコンテンツ
</div>
<div role="tabpanel" class="tab-panel">
タブ3のコンテンツ
</div>
</div>
.tab-panel {
interpolate-size: allow-keywords;
height: 0;
opacity: 0;
overflow: hidden;
transition:
height 0.3s ease,
opacity 0.3s ease;
}
.tab-panel.active {
height: auto;
opacity: 1;
}
カルーセル
.carousel-item {
interpolate-size: allow-keywords;
width: 0;
overflow: hidden;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.carousel-item.active {
width: auto;
}
まとめ
interpolate-sizeは、長年の課題だったauto値のアニメーションを可能にする革新的な機能です。
主な利点
- シンプルな実装: JavaScriptなしでautoのアニメーションが可能
- パフォーマンス: ブラウザネイティブの最適化
- 保守性: コンテンツサイズの変更に自動対応
- 柔軟性: 高さ・幅の両方に適用可能
今後の展望
現在は一部のブラウザでのみサポートされていますが、今後の普及が期待されます。プログレッシブエンハンスメントを活用し、サポートするブラウザでは最適な体験を、それ以外ではフォールバックを提供する実装を心がけましょう。
次のプロジェクトでinterpolate-sizeを試して、より洗練されたUIアニメーションを実現してください。