<section class="accordion js-accordion" aria-labelledby="heading1">
  <h3 id="heading1" class="heading">
    <a class="trigger" role="button" href="#panel1" aria-expanded="false" aria-controls="panel1">
      EVOLABとは
      <span class="icon" role="img" aria-hidden="true"></span>
    </a>
  </h3>
  <div id="panel1" class="panel" hidden="until-found">
    <div class="panel-inner">
      <p>EVOWORXのエンジニアによるWeb制作会社向けの情報ポータルです。</p>
    </div>
  </div>
</section>
<section class="accordion js-accordion" aria-labelledby="heading2">
  <h3 id="heading2" class="heading">
    <a class="trigger" role="button" href="#panel2" aria-expanded="false" aria-controls="panel2">
      BLOG
      <span class="icon" role="img" aria-hidden="true"></span>
    </a>
  </h3>
  <div id="panel2" class="panel" hidden="until-found">
    <div class="panel-inner">
      <p>EVOWORXのエンジニアによるブログをお届けします。</p>
    </div>
  </div>
</section>
<section class="accordion js-accordion" aria-labelledby="heading3">
  <h3 id="heading3" class="heading">
    <a class="trigger" role="button" href="#panel3" aria-expanded="false" aria-controls="panel3">
      SAMPLE
      <span class="icon" role="img" aria-hidden="true"></span>
    </a>
  </h3>
  <div id="panel3" class="panel" hidden="until-found">
    <div class="panel-inner">
      <p>コピー&amp;ペーストで使えるコンポーネントのサンプルです。</p>
    </div>
  </div>
</section>
<section class="accordion js-accordion" aria-labelledby="heading4">
  <h3 id="heading4" class="heading">
    <a class="trigger" role="button" href="#panel4" aria-expanded="false" aria-controls="panel4">
      LAB
      <span class="icon" role="img" aria-hidden="true"></span>
    </a>
  </h3>
  <div id="panel4" class="panel" hidden="until-found">
    <div class="panel-inner">
      <p>アニメーションなどのコードの実験室です。</p>
    </div>
  </div>
</section>
<section class="accordion js-accordion" aria-labelledby="heading5">
  <h3 id="heading5" class="heading">
    <a class="trigger" role="button" href="#panel5" aria-expanded="false" aria-controls="panel5">
      GUIDELINE &amp; TIPS
      <span class="icon" role="img" aria-hidden="true"></span>
    </a>
  </h3>
  <div id="panel5" class="panel" hidden="until-found">
    <div class="panel-inner">
      <p>弊社のコーディングガイドラインやWeb制作のお役立ち情報です。</p>
    </div>
  </div>
</section>
.accordion {
  background-color: #fff;
  border-radius: 10px;
  padding: 20px;

  &:not(:first-of-type) {
    margin-top: 30px;
  }

  .trigger {
    width: 100%;
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    column-gap: 12px;
    color: #333;
    font-weight: 700;
    text-decoration: none;

    &::before {
      content: "Q";
      width: 35px;
      display: grid;
      place-content: center;
      aspect-ratio: 1;
      background-color: #333;
      border-radius: 50%;
      color: #fff;
      font-size: 20px;
      font-weight: 700;
      position: relative;
    }

    &[aria-expanded="true"] {
      .icon {
        &::before {
          rotate: 180deg;
        }
      }
    }

    .icon {
      width: 16px;
      aspect-ratio: 1;
      position: relative;

      &::before,
      &::after {
        content: "";
        display: block;
        width: 100%;
        height: 2px;
        background-color: #333;
        position: absolute;
        inset: 0;
        margin: auto;
        border-radius: 16px;
      }

      &::before {
        rotate: 90deg;
        transition: rotate 0.3s ease;
      }
    }
  }

  .panel {
    display: block;
    overflow: clip;
    transition: height 0.4s ease;

    .panel-inner {
      padding-top: 32px;
      padding-bottom: 4px;
    }
  }
}

body {
  background-color: #333;
  padding: 30px;
}
View Compiled
class Accordion {
  constructor(el) {
    // DOM要素
    this.el = el;
    if (!this.el) return;
    this.trigger = null;
    this.panel = null;
    // インスタンス
    this.observer = null;
    // フラグ
    this.isOpen = null;
    this.isAnimating = false;
  }

  // 初期化
  init() {
    this.trigger = this.el.querySelector('[role="button"][aria-expanded]');
    this.panel = this.el.querySelector(
      `#${this.trigger.getAttribute("aria-controls")}`
    );
    this.isOpen = !this.panel.hidden;

    this.panel.style.height = this.isOpen ? "" : "0";
    this._observeHidden();
    this.trigger.addEventListener("click", this._toggle.bind(this));
    this.trigger.addEventListener("keydown", (e) => {
      e.key === " " && this._toggle();
    });
  }

  // 破棄
  destroy() {
    this.isAnimating = false;
    if (this.trigger) {
      this.trigger.removeEventListener("click", this._toggle.bind(this));
      this.trigger.removeEventListener("keydown", this._toggle.bind(this));
      this.trigger = null;
    }
    if (this.panel) {
      this.panel = null;
    }
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }

  // アニメーションの待機
  async _waitAnimation(target) {
    const animations = target.getAnimations();
    if (animations.length === 0) {
      return Promise.resolve();
    } else {
      await Promise.allSettled(
        animations.map((animation) => animation.finished)
      );
    }
  }

  // アコーディオンの開閉
  async _toggle(e) {
    e.preventDefault();
    if (this.isAnimating) return;

    this.isAnimating = true;
    this.isOpen = !this.isOpen;
    this.trigger.ariaExpanded = this.isOpen;

    if (this.isOpen) {
      this.panel.hidden = false;
      this.panel.style.height = `${this.panel.scrollHeight}px`;
      await this._waitAnimation(this.panel);
      this.panel.style.height = "auto";
      this.isAnimating = false;
    } else {
      this.panel.style.height = `${this.panel.scrollHeight}px`;
      requestAnimationFrame(() => {
        requestAnimationFrame(async () => {
          this.panel.style.height = "0";
          await this._waitAnimation(this.panel);
          this.panel.hidden = "until-found";
          this.isAnimating = false;
        });
      });
    }
  }

  // hidden属性の監視(ページ内検索用)
  _observeHidden() {
    this.observer = new MutationObserver(() => {
      if (this.isAnimating) return;
      this.isOpen = !this.panel.hidden;
      this.trigger.ariaExpanded = this.isOpen;
      this.panel.style.height = this.isOpen ? "auto" : "0";
    });

    this.observer.observe(this.panel, {
      attributes: true,
      attributeFilter: ["hidden"]
    });
  }
}

// クラスの実行
const elements = document.querySelectorAll(".js-accordion");

elements.forEach((el) => {
  const accordion = new Accordion(el);
  accordion.init();
});
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.