U-Code

<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 } // イベントを一度だけリスン
      );
    }
  });
});