アコーディオンUI(展開/折りたたみ)はよく見られるインタラクションの一つです。通常はdisplayプロパティをnoneと他の値で切り替えますが、アニメーションがなくてぎこちない印象を与えます。そこで、スムーズな展開・収縮アニメーションを実装したいというニーズが生まれます。
実装手法
1. max-height を利用する
まず思いつくのはheightを0とautoで切り替える方法ですが、これは期待通りに動作しません。なぜなら、展開する要素の高さは動的で固定値が分からないからです。0pxからautoへの遷移は計算できず、トランジションやアニメーションが適用されません。
そこでmax-heightを使います。max-heightを0から、内部要素を完全に表示できる値(実際の高さより大きい値)に変化させます。要素の実際の高さはautoのまま維持されるため、高さが不定の要素でもアニメーションが可能になります。
ただし注意点として、max-heightの値が大きすぎると、折りたたむときに実際の高さまで戻るのに時間がかかり、遅延が生じます。そのため、安全な最小値(十分に余裕のある値)に設定することを推奨します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アコーディオン(max-height版)</title>
<style>
.card {
width: 240px;
padding: 12px 24px;
border: 1px solid #ddd;
font-family: sans-serif;
}
.wrapper {
overflow: hidden;
}
.wrapper > .content {
transition: max-height 0.4s ease;
max-height: 0;
}
.wrapper > .content.open {
max-height: 200px; /* 安全な最小値 */
}
.toggle {
color: #3b82f6;
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="card">
<div class="wrapper">
<span class="toggle" onclick="togglePanel(this)" data-state="closed">開く</span>
<div class="content">
<div>項目A</div>
<div>項目B</div>
<div>項目C</div>
<div>項目D</div>
<div>項目E</div>
</div>
</div>
</div>
<script>
function togglePanel(btn) {
const panel = btn.nextElementSibling;
const state = btn.dataset.state;
if (state === 'closed') {
btn.textContent = '閉じる';
btn.dataset.state = 'open';
panel.classList.add('open');
} else {
btn.textContent = '開く';
btn.dataset.state = 'closed';
panel.classList.remove('open');
}
}
</script>
</body>
</html>
2. 実測 height を取得してトランジション
max-heightには限界があるため、DOM読み込み後に実際の高さを取得し、それを0との間でアニメーションさせる方法があります。JavaScriptの実行はDOMレンダリングをブロックするため、高さを取得してから0に設定するまでの間にちらつきが発生することはほとんどありません。もし心配なら、要素を複製して画面外で高さを取得する方法も使えます。
以下の例では、DOMContentLoaded時に要素の実際の高さを取得し、それをdata-*属性に保存してから高さを0に設定します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アコーディオン(実測 height版)</title>
<style>
.card {
width: 240px;
padding: 12px 24px;
border: 1px solid #ddd;
font-family: sans-serif;
}
.wrapper {
overflow: hidden;
}
.wrapper > .content {
transition: height 0.4s ease;
height: 0;
}
.toggle {
color: #3b82f6;
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="card">
<div class="wrapper">
<span class="toggle" onclick="togglePanel(this)" data-state="closed">開く</span>
<div class="content" data-realheight="">
<div>項目A</div>
<div>項目B</div>
<div>項目C</div>
<div>項目D</div>
<div>項目E</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const panel = document.querySelector('.content');
const realHeight = window.getComputedStyle(panel).height;
panel.dataset.realheight = realHeight;
panel.style.height = '0px'; // 初期状態は閉じている
});
function togglePanel(btn) {
const panel = btn.nextElementSibling;
const state = btn.dataset.state;
const realHeight = panel.dataset.realheight;
if (state === 'closed') {
btn.textContent = '閉じる';
btn.dataset.state = 'open';
panel.style.height = realHeight;
} else {
btn.textContent = '開く';
btn.dataset.state = 'closed';
panel.style.height = '0px';
}
}
</script>
</body>
</html>
3. translateY でスライドダウンアニメーション
別のアプローチとして、外側の要素を最初から高さ0にしておき(overflow:hidden)、内部要素をtransform: translateY(-100%)で上に隠しておき、translateY(0)にすることでスライドダウンさせる方法があります。これはWeUIなどで使われている手法です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アコーディオン(translateY版)</title>
<style>
.card {
width: 240px;
padding: 12px 24px;
border: 1px solid #ddd;
font-family: sans-serif;
}
.outer-container {
overflow: hidden;
height: 0;
transition: height 0.3s ease;
}
.outer-container.open {
height: auto; /* 高さは自動 */
}
.inner-slider {
transform: translateY(-100%);
transition: transform 0.4s ease;
}
.inner-slider.visible {
transform: translateY(0);
}
.toggle {
color: #3b82f6;
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="card">
<div class="wrapper">
<span class="toggle" onclick="togglePanel(this)" data-state="closed">開く</span>
<div class="outer-container">
<div class="inner-slider">
<div>項目A</div>
<div>項目B</div>
<div>項目C</div>
<div>項目D</div>
<div>項目E</div>
</div>
</div>
</div>
</div>
<script>
function togglePanel(btn) {
const outer = btn.nextElementSibling;
const inner = outer.querySelector('.inner-slider');
const state = btn.dataset.state;
if (state === 'closed') {
btn.textContent = '閉じる';
btn.dataset.state = 'open';
outer.classList.add('open');
inner.classList.add('visible');
} else {
btn.textContent = '開く';
btn.dataset.state = 'closed';
outer.classList.remove('open');
inner.classList.remove('visible');
}
}
</script>
</body>
</html>