ウェブサービスを作っています。

details 要素 + Stimulus で開閉アニメーション表示をする (高さ可変のアコーディオンメニュー)

<details> 要素 を使うと JavaScript を使わずにアコーディオンメニューを実装することができます。

ただ、2022年2月時点では、開閉のアニメーション表示をする組み込みの方法がありません。

そこで、Stimulus を使って開閉アニメーション表示をするコードを示します。


HTML

<details data-controller="accordion">
  <summary data-accordion-target="summary" data-action="click->accordion#toggle">詳細</summary>
  <div data-accordion-target="content">内容</div>
</details>

accordion_controller.js

import { Controller } from "@hotwired/stimulus"

// Thanks to: https://css-tricks.com/how-to-animate-the-details-element-using-waapi/

export default class extends Controller {
  static targets = ['summary', 'content']

  connect() {
    this.resetState()
  }

  resetState() {
    this.animation = null
    this.isClosing = false
    this.isExpanding = false
  }

  toggle(event) {
    event.preventDefault()

    this.element.style.overflow = 'hidden'

    if (this.isClosing || !this.element.open) {
      this.open()
    } else if (this.isExpanding || this.element.open) {
      this.shrink()
    }
  }

  open() {
    this.element.style.height = `${ this.element.offsetHeight }px`
    this.element.open = true

    window.requestAnimationFrame(() => this.expand())
  }

  expand() {
    this.isExpanding = true

    const startHeight = `${ this.element.offsetHeight }px`
    const endHeight = `${ this.summaryTarget.offsetHeight + this.contentTarget.offsetHeight }px`

    if (this.animation) {
      this.animation.cancel()
    }

    this.animation = this.animate(startHeight, endHeight)

    this.animation.onfinish = () => this.onAnimationFinish(true)
    this.animation.oncancel = () => this.isExpanding = false
  }

  shrink() {
    this.isClosing = true

    const startHeight = `${ this.element.offsetHeight }px`
    const endHeight = `${ this.summaryTarget.offsetHeight }px`

    if (this.animation) {
      this.animation.cancel()
    }

    this.animation = this.animate(startHeight, endHeight)

    this.animation.onfinish = () => this.onAnimationFinish(false)
    this.animation.oncancel = () => this.isClosing = false
  }

  animate(startHeight, endHeight) {
    return this.element.animate({
      height: [startHeight, endHeight]
    }, {
      duration: 300,
      easing: 'ease-out'
    })
  }

  onAnimationFinish(open) {
    this.element.open = open

    this.resetState()
    this.element.style.height = this.element.style.overflow = ''
  }
}

参考