<!-- 1. aria属性を使った書き方 -->
<div class="Accordion">
  <button class="Accordion__Summary" data-accordion aria-controls="some-id" aria-expanded="false">
    <span>1. div, button, aria属性を使用して実装したアコーディオン</span>
  </button>
  <div class="Accordion__Content" id="some-id" aria-hidden="true">
    <p>開閉状態は aria-expanded の値で判定します。</p>
  </div>
</div>

<!-- 2. details, summary を使った書き方 -->
<details class="Accordion">
  <summary class="Accordion__Summary">
    <span>2. details, summaryを使用したアコーディオン</span>
  </summary>
  <div class="Accordion__Content">
    <p>開閉状態は details タグの open 属性の有無で判定します。</p>
  </div>
</details>


* {
  box-sizing: border-box;
}

// クリック可能要素をホバーしたときにカーソルをポインターにする
// 1.の場合button, 2.の場合summary
button,
summary {
  cursor: pointer;
}

// 1.の場合、buttonのスタイルリセット
button {
  border: none;
}

// 2.の場合、summaryの「▼」をリセット
summary {
  list-style: none;
  &::-webkit-details-marker {
    display: none;
  }
}

.Accordion {
  margin: 20px;
  border: 2px solid #ccf;
}

.Accordion__Summary {
  width: 100%;
  font-size: 18px;
  position: relative;
  padding: 20px;
  background-color: #ccf;
  color: #33c;
  text-align: left;
  
  // 「+」「-」の装飾
  &::before,
  &::after {
    position: absolute;
    top: 50%;
    display: block;
    width: 16px;
    height: 2px;
    content: "";
    background-color: currentColor;
    right: 30px;
  }
  &::before {
    transform: translateY(-50%);
  }
  &::after {
    transition: transform 0.5s;
    transform: translateY(-50%) rotate(-90deg);
  }
  // 1. の場合
  &[aria-expanded="true"]::after {
    transform: translateY(-50%);
  }
  // 2. の場合
  .Accordion[open]:not([data-accordion-before-close]) &::after {
    transform: translateY(-50%);
  }
}


.Accordion__Content {
  overflow: hidden;
  
  > p {
    padding: 20px;
  }
}

View Compiled
import gsap from "https://cdn.skypack.dev/gsap@3.10.4";

// 1. aria属性を使った書き方
document.querySelectorAll("[data-accordion]").forEach((button) => {
  const content = document.getElementById(button.getAttribute("aria-controls"));

  const onClick = () => {
    if (button.getAttribute("aria-expanded") === "false") {
      button.setAttribute("aria-expanded", true);
      content.setAttribute("aria-hidden", false);
      gsap.fromTo(
        content,
        { height: 0, clearProps: "display" },
        { height: "auto" }
      );
    } else {
      button.setAttribute("aria-expanded", false);
      content.setAttribute("aria-hidden", true);
      gsap.to(content, { height: 0, display: "none" });
    }
  };
  button.addEventListener("click", onClick);

  // 初期化
  if (button.getAttribute("aria-expanded") === "false") {
    content.setAttribute("aria-hidden", true);
    gsap.set(content, { height: 0, display: "none" });
  }
});

// 2. details, summary を使った書き方
// ※アニメーションが必要なければ、JSは不要
document.querySelectorAll("details").forEach((details) => {
  const summary = details.querySelector("summary");
  const content = details.querySelector("summary + *");

  const onClick = (event) => {
    if (details.open) {
      // 閉じるアニメーションのみ preventDefault が必要
      event.preventDefault();
      details.setAttribute("data-accordion-before-close", "");
      gsap.to(content, {
        height: 0,
        onComplete: () => {
          details.open = false;
          details.removeAttribute("data-accordion-before-close");
        },
      });
    } else {
      gsap.fromTo(content, { height: 0 }, { height: "auto" });
    }
  };

  summary.addEventListener("click", onClick, { passive: false });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.