Feylo

Accordion

コンテンツを折りたたんで表示するコンポーネントです。details要素を使って作成します。ここではJavaScriptを使用したアニメーションの方法も紹介します。

Accordion

アコーディオンを作る前提

このページでは、アコーディオンをHTMLのdetails要素とsummary要素で作る前提で説明します。detailssummaryを合わせて使えば、簡単にアコーディオンや折りたたみメニューを作ることができます。

アコーディオンを作成する際にdetailsとsummaryで実装するメリット

detailssummaryはHTML5から追加された要素ですが、IEでは対応していませんでした。なので、IEサポート終了前までは、アコーディオンはdiv要素やinput要素のtype="checkbox"などを利用して作成されてました。

ですが、これらの要素ではアクセシビリティなどの問題がありますので、今後はdetailssummaryを使ってアコーディオンを作成することが推奨されています。

アクセシビリティ面で、アコーディオンをdetailsとsummaryで実装するメリットに関しては、以下の通りです。

メリット
  • JavaScriptを使用しなくても、キーボード操作でアコーディオンを開閉できる。
  • サイト内検索で、ヒットした単語の含まれるアコーディオンが開く。
  • スクリーンリーダーが開閉状態について適切に読み上げてくれる。

以上のように、アコーディオンをdetailssummaryで実装することで、より良いアクセシビリティを実現することができます。それでは使い方を見ていきましょう。

detailsとsummaryタグの使い方

detailssummaryを以下のように合わせて使うことで、簡単にアコーディオンを作ることができます。

各要素の説明は以下の通りです。

要素説明
detailsアコーディオン全体を囲む
summaryアコーディオンのタイトルを入れます。アイコンなどを設定する場合はこちらに入れます。
pアコーディオンの中身を入れます。段落(p)だけではなく、リスト(ul, ol)やdivなども入れることができます。

初期状態を開いた状態にする

details要素にopen属性を付けることで、アコーディオンの初期状態を開いた状態にすることができます。

CSSでアコーディオンをカスタマイズ

detailssummaryで作ったアコーディオンは、デフォルトでは上記のようにタイトル(summary)の横に三角形の矢印が表示されます。これをそのまま使うことはないので、矢印を消したり、矢印のカスタマイズをしたり、開いた状態のスタイルを変更する方法を紹介します。

矢印を消す

三角形の矢印を消す方法は、Safari以外はsummary要素にdisplay: blockを付けることで削除できますが、Safariでは消せません。

Safariでは、summary::-webkit-details-markerによって矢印が表示されているので、これをdisplay: noneすることで矢印を消すことができます。

矢印のカスタマイズ

矢印をカスタマイズします。矢印は、summary要素の中にiconクラスを使って作ります。ここでは、矢印は「+」の形にします。位置の設定にはgridを使います。

「+」の形にするには、iconクラスのbefore要素とafter要素を使って、position: absoluteで位置を調整します。

開いた状態のスタイルを変更する

アコーディオンが開いた状態が分かるように、「+」のアイコンを「-」に変更します。アコーディオンが開いた状態のスタイルは、details[open]を使って指定することができます。

CSS
.icon {
  &::after {
    // ...
    transition-property: rotate;
    transition-duration: 0.3s;
  }
}

details[open] .icon {
  &::after {
    rotate: 90deg;
  }
}

↓のようになります。

アコーディオンに開閉アニメーションを付ける

そのままdetailssummaryで作ったアコーディオンには、開閉の時にアニメーションがなく即時に開閉してしまいます。そこで、開閉のアニメーションを付けてみましょう。

この記事によると、::details-content疑似要素を利用することで、JavaScriptを使わずにCSSのみでアニメーションを付けることができます。

ですがSafariとFirefoxでは、記事作成時(2025/07)で::details-contentを利用したアニメーションが動作しないので、ここではJavaScriptを使ってアニメーションを付ける方法を紹介します。

全ブラウザーで動作するようになったら、:::details-contentを使ったアニメーションも紹介します。

JavaScriptで開閉アニメーションを付ける方法

JavaScriptでアコーディオンの開閉アニメーションを付ける方法を紹介します。ここでは、複数のアコーディオンに対しても対応できるようにします。CodePenのデモは下記になります。

アニメーションではJavaScriptでアコーディオンの中身の高さを取得して、Web Animations APIを使ってアニメーションを付けています。

HTML

このデモのHTMLは以下のようになります。

HTML
<div class="accordion js-accordion">
  <details class="accordion__details js-details">
    <summary class="js-summary">
      Sample01
      <span class="accordion__icon"></span>
    </summary>
    <div class="accordion__content js-content">
      <div class="accordion__content-inner">
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
        <ul>
          <li>Lorem ipsum dolor sit amet.</li>
          <li>Lorem, ipsum dolor sit amet consectetur adipisicing elit.</li>
        </ul>
      </div>
    </div>
  </details>
</div>

// ...

JavaScriptで操作する要素には、js-から始まるクラスを付けておきます。

CSS

CSSに関しては長くなるので、全てのコードはCodePenの方を確認ください。
ここでは、先ほどの説明から変更があった箇所として、アコーディオンが開いた時に「+」から「-」に変わるアニメーションについてだけ説明します。

JavaScriptで時間差のアニメーションを付ける影響で、details要素のopen属性だけを見ると、アコーディオンが閉じる際のアニメーションは、全て閉じ終わってから「+」に変わるアニメーションが始まります。

これを回避するために、アコーディオンの開閉の際に、is-openedクラスを付け外すようにします。なので、details要素にis-openedクラスが付いたらアニメーションするようにCSSを記述しましょう。

SCSS
.accordion__details.is-opened {
  .accordion__icon {
    &::after {
      rotate: 90deg;
    }
  }
}

JavaScript

最初にJavaScriptの全コードを下記に示します。

JavaScript
class Accordion {
  constructor() {
    this.elms = document.querySelectorAll('.js-accordion');
    if (this.elms.length) {
      this.elms.forEach(elm => this.init(elm))
    }
  }

  init(_elm) {
    const elm = _elm;
    this.setAnimTiming(400, 'ease-out');
    this.defaultAccordion(elm);
  }

  setAnimTiming(_duration, _easing) {
    this.animTiming = {
      duration: _duration,
      easing: _easing
    };
  }

  defaultAccordion(_elm) {
    this.RUNNING_VALUE = 'running';
    this.IS_OPENED_CLASS = 'is-opened';
    this.options = this.animTiming;

    const details = _elm.querySelector('.js-details');
    const content = _elm.querySelector('.js-content');

    details.addEventListener('click', (e) => {
      e.preventDefault();

      // 連打防止用。アニメーション中だったらクリックイベントを受け付けないでリターン
      if (details.dataset.animStatus === this.RUNNING_VALUE) {
        return;
      }

      details.open ? this.close(details, content) : this.open(details, content);
    })
  }

  close(details, content) {
    details.classList.toggle(this.IS_OPENED_CLASS);
    const closingAnim = content.animate(this.closingAnimKeyframes(content), this.options);
    details.dataset.animStatus = this.RUNNING_VALUE;

    closingAnim.onfinish = () => {
      details.open = false;
      details.dataset.animStatus = '';
    }
  }

  open(details, content) {
    details.open = true;
    details.classList.toggle(this.IS_OPENED_CLASS);
    const openingAnim = content.animate(this.openingAnimKeyframes(content), this.options);
    details.dataset.animStatus = this.RUNNING_VALUE;

    openingAnim.onfinish = () => {
      details.dataset.animStatus = '';
    }
  }

  closingAnimKeyframes(content) {
    return [
      { height: content.offsetHeight + 'px' },
      { height: 0 }
    ];
  }

  openingAnimKeyframes(content) {
    return [
      { height: 0 },
      { height: content.offsetHeight + 'px' }
    ];
  }
}

new Accordion();

先述の通り、Web Animations APIを使ってアニメーションを付けています。
それでは、コードの解説をしていきます。

constructor()

constructor()では、複数のアコーディオンに対応できるように、querySelectorAllを使って全てのアコーディオンを取得し、init()に渡しています。

JavaScript
constructor() {
  this.elms = document.querySelectorAll('.js-accordion');
  if (this.elms.length) {
    this.elms.forEach(elm => this.init(elm))
  }
}

init()

init()では、アニメーションのタイミングを設定して、defaultAccordion()に渡しています。

JavaScript
init(_elm) {
  const elm = _elm;
  this.setAnimTiming(400, 'ease-out');
  this.defaultAccordion(elm);
}

setAnimTiming()

setAnimTiming()では、Web Animations APIで使用するオプションを設定しています。

JavaScript
setAnimTiming(_duration, _easing) {
  this.animTiming = {
    duration: _duration,
    easing: _easing
  };
}

defaultAccordion()

defaultAccordion()では、アコーディオンのクリックイベントなどを設定しています。
また連打防止用に、アニメーション中だったらクリックイベントを受け付けないようにdetails要素のdataset.animStatusの値をチェックしています。

JavaScript
defaultAccordion(_elm) {
  this.RUNNING_VALUE = 'running';
  this.IS_OPENED_CLASS = 'is-opened';
  this.options = this.animTiming;

  const details = _elm.querySelector('.js-details');
  const content = _elm.querySelector('.js-content');

  details.addEventListener('click', (e) => {
    e.preventDefault();

    // 連打防止用。アニメーション中だったらクリックイベントを受け付けないでリターン
    if (details.dataset.animStatus === this.RUNNING_VALUE) {
      return;
    }

    details.open ? this.close(details, content) : this.open(details, content);
  })
}

details要素のopenプロパティの値によって、open()close()を呼び出しています。

open()・close()

open()close()では、Web Animation APIでアニメーションを実行しています。

JavaScript
close(details, content) {
  details.classList.toggle(this.IS_OPENED_CLASS);
  const closingAnim = content.animate(this.closingAnimKeyframes(content), this.options);
  details.dataset.animStatus = this.RUNNING_VALUE;

  closingAnim.onfinish = () => {
    details.open = false;
    details.dataset.animStatus = '';
  }
}

open(details, content) {
  details.open = true;
  details.classList.toggle(this.IS_OPENED_CLASS);
  const openingAnim = content.animate(this.openingAnimKeyframes(content), this.options);
  details.dataset.animStatus = this.RUNNING_VALUE;

  openingAnim.onfinish = () => {
    details.dataset.animStatus = '';
  }
}

それぞれ連打防止用のクラスを付けたり、アニメーションのオプションを設定したりしています。

animate()の第1引数には、アニメーションのキーフレームを配列で渡します。closeopenのそれぞれのアニメーションキーフレームはclosingAnimKeyframes()openingAnimKeyframes()で設定しています。ここで、アコーディオンの中身の高さを取得するので、contentを渡しています。

JavaScript
closingAnimKeyframes(content) {
  return [
    { height: content.offsetHeight + 'px' },
    { height: 0 }
  ];
}

openingAnimKeyframes(content) {
  return [
    { height: 0 },
    { height: content.offsetHeight + 'px' }
  ];
}

参考サイト