<details><summary>を使用したアコーディオン
コーディング
アクセシビリティに配慮した<details><summary>を使用したアコーディオンのメモです。
HTML / CSS と素のJavaScriptで記述しています。
デモです
https://u-coded.github.io/accordion/
HTML
注意点
summaryのaria-controlsは、開くコンテンツ部分のidと一致させる
<details class="details" data-details>
<summary class="summary" aria-controls="panel-1" aria-expanded="false">
1つめのアコーディオン
</summary>
<div id="panel-1" class="details-content" aria-hidden="true" data-details-content>
<div class="details-content-inner">
<p class="details-content-txt">1つめのアコーディオンの中身</p>
</div>
</div>
</details>
<details class="details" data-details>
<summary class="summary" aria-controls="panel-2" aria-expanded="false">
2つめのアコーディオン
</summary>
<div id="panel-2" class="details-content" aria-hidden="true" data-details-content>
<div class="details-content-inner">
<p class="details-content-txt">2つめのアコーディオンの中身</p>
</div>
</div>
</details>
CSS
最低限のスタイルのみ適用しているため、適宜調整してください。
/* Safariのデフォルトの矢印を非表示にする */
summary::-webkit-details-marker {
display: none;
}
.details {
margin-top: 30px;
overflow: clip;
cursor: pointer;
}
.details[open] .summary::after {
scale: 1 -1;
}
.details[open] .details-content {
height: auto;
}
.summary {
display: grid;
padding: 10px;
border: 1px solid;
position: relative;
grid-template-columns: 1fr auto;
}
.summary::after {
content: "";
clip-path: polygon(50% 100%, 0 0, 100% 0);
width: 10px;
height: 10px;
position: absolute;
top: 50%;
right: 10px;
translate: 0 -50%;
transition: scale 0.15s;
background: black;
}
.details-content-inner {
padding: 10px;
}
JavaScript
処理の内容については、ソース内のコメントを参考にしてください。
const DETAILS_SEL = "[data-details]"; // <details> 要素のセレクター
const DETAILS_CONTENT_SEL = "[data-details-content]"; // コンテンツ部分のセレクター
// 全ての <details> 要素を取得
const detailsElements = document.querySelectorAll(DETAILS_SEL);
detailsElements.forEach((detail) => {
const summary = detail.querySelector("summary"); // 各 <summary> 要素を取得
const content = detail.querySelector(DETAILS_CONTENT_SEL); // 各コンテンツ部分を取得
// ページロード時の初期状態設定
if (detail.hasAttribute("open")) {
content.style.height = `${content.scrollHeight}px`; // 開いている場合、コンテンツの高さを設定
} else {
content.style.height = "0px"; // 閉じている場合、コンテンツの高さを0に設定
}
// <summary> クリック時の処理
summary.addEventListener("click", (e) => {
e.preventDefault(); // デフォルトのクリック動作を防ぐ
const isOpen = detail.hasAttribute("open"); // <details> が開いているかどうかを判定
const duration = 150; // 継続時間
if (!isOpen) {
// <details> が閉じている場合
detail.setAttribute("open", "true"); // open 属性を追加
summary.setAttribute("aria-expanded", "true"); // アクセシビリティ属性を更新
content.setAttribute("aria-hidden", "false"); // アクセシビリティ属性を更新
content.style.height = "auto"; // 自動高さに設定し直す
const contentHeight = content.scrollHeight; // 現在の高さを取得
content.style.height = "0px"; // 高さを0に設定してからアニメーションを開始
requestAnimationFrame(() => {
content.style.transition = `height ${duration}ms ease`; // アニメーションの設定
content.style.height = `${contentHeight}px`; // 実際の高さにアニメーション
});
content.addEventListener(
"transitionend",
() => {
content.style.height = "auto"; // アニメーション終了後に高さを自動に戻す
},
{ once: true } // イベントを一度だけリスン
);
} else {
// <details> が開いている場合
const contentHeight = content.scrollHeight; // 現在の高さを取得
content.style.height = `${contentHeight}px`; // アニメーション開始前に現在の高さを設定
requestAnimationFrame(() => {
content.style.transition = `height ${duration}ms ease`; // アニメーションの設定
content.style.height = "0px"; // 高さを0にアニメーション
});
content.addEventListener(
"transitionend",
() => {
detail.removeAttribute("open"); // open 属性を削除
summary.setAttribute("aria-expanded", "false"); // アクセシビリティ属性を更新
content.setAttribute("aria-hidden", "true"); // アクセシビリティ属性を更新
content.style.height = ""; // 高さをリセット
},
{ once: true } // イベントを一度だけリスン
);
}
});
});