🪗

アニメーションで開閉する Vue.js アコーディオンコンポーネント

2024/02/15に公開

はじめに

タイトルの通り、Vue.js で使えるアコーディオンコンポーネントを紹介します。
このようなコンポーネントはよく使うのですが、毎回考えて書くのは若干面倒なので記事としてまとめておきます。

プリプロセッサ

コード内では、以下のプリプロセッサを使っています。

  • pug
  • TypeScript
  • sass

コンポーネントのコード

アニメーションはvueの組み込みコンポーネントであるtransitionを使って実装します。
これにより、閉じている時は要素自体がDOMから消えるので、色々とスッキリします。

shrinkable-container.vue
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'

const props = defineProps<{
  shrunk?: boolean
}>()

const inner = ref<HTMLDivElement>()
const innerHeightCss = ref('')
const initialized = ref(false)

const calcHeight = () => {
  if (!inner.value) return
  innerHeightCss.value = inner.value.getBoundingClientRect().height + 'px'
  initialized.value = true
}

watch(
  () => props.shrunk,
  () => {
    nextTick(calcHeight)
  }
)

onMounted(calcHeight)
</script>

<template lang="pug">
transition
  .shrinkable-container(
    v-if="!shrunk"
    :class="{ initial: !initialized }"
  )
    .inner(
      ref="inner"
    )
      slot
</template>

<style lang="sass" scoped>
.v-enter-active,
.v-leave-active
  transition: height 0.5s

.v-enter-from,
.v-leave-to
  height: 0

.v-enter-to,
.v-leave-from
  height: v-bind(innerHeightCss)

.shrinkable-container
  overflow: clip
  width: fit-content
  &.initial
    transition: none
</style>

使い方

test-component.vue
<script setup lang="ts">
import { ref } from 'vue'
import ShrinkableContainer from './shrinkable-container.vue'

const isOpen = ref(false)
</script>

<template lang="pug">
.test
  button(
    @click="isOpen = !isOpen"
  ) Toggle Open
  ShrinkableContainer(
    :shrunk="!isOpen"
  )
    .content Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vel augue a quam iaculis tristique non vel est. Cras eu interdum sapien, eu bibendum felis. Suspendisse ac aliquet velit. Nulla lorem elit, euismod id massa facilisis, convallis dictum lectus. Nulla suscipit viverra risus, sit amet varius nisl mollis sit amet. Maecenas rutrum suscipit mi quis porttitor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus cursus nibh quis sapien varius mollis. Morbi id purus sodales, vulputate nibh non, mollis eros. Proin feugiat mi vel porttitor tempor.
</template>

gridを使うともっと楽?

今回は高さが可変な要素の高さを毎回計算してアニメーションする方式にしていますが、display:gridを使ったもっと簡単な実装も存在するようです。

https://coliss.com/articles/build-websites/operation/css/css-transition-from-height-0-to-auto.html

ただ、こちらを試したところ開閉のアニメーション時に謎の隙間ができるという現象に遭遇したため、使用を断念しました…
この方法ではコンテナの中にインナーとなる要素を設けていますが、コンテナのアニメーションとインナー要素のアニメーションがなぜかずれてしまうようです。

CodePen上で動かしているデモがありますが、CSSのコード文末に以下を追加して動かすと分かりやすいです。
期待値としては赤のラインと黄色のラインの上下端がピッタリくっついて動いて欲しいのですが、実際には隙間ができてしまいます。

https://codepen.io/francescovetere/pen/BaGwBRx

.accordion-body {
  border: solid 1px red;
  transition: 1s grid-template-rows ease;
}
.accordion-body > div {
  border: solid 1px yellow;
}

この実装で期待通りに動けば、CSSのみで完結するようになるのでとても便利なのですが…

おわりに

いつかまとめようと思って随分と時間が経ってしまいましたが、このコードが皆様および未来の自分の作業時間を1msecでも短縮できれば幸いです。

Discussion