/vue-collapsed

CSS height transition from any to auto and vice versa. Accordion ready.

Primary LanguageVueMIT LicenseMIT

npm GitHub Workflow Status npm bundle size dependency-count

Vue Collapsed

Dynamic CSS height transition from any to auto and vice versa. Accordion ready.

Examples and Demo - Stackblitz

💡 Requires Vue v3.0.0 or above.


Get Started

npm i -S vue-collapsed
# yarn add vue-collapsed
# pnpm add vue-collapsed

Register it globally:

import { Collapse } from 'vue-collapsed'

createApp(App).component('Collapse', Collapse).mount('#app')

Or import it locally:

import { Collapse } from 'vue-collapsed'

Props

name description type required
when Value to control collapse boolean
baseHeight Collapsed height in px, defaults to 0. number
as Tag to use instead of div keyof HTMLElementTagNameMap
onExpand Callback on expand transition start () => void
onExpanded Callback on expand transition completed () => void
onCollapse Callback on collapse transition start () => void
onCollapsed Callback on collapse transition completed () => void

Usage

<script setup>
import { ref } from 'vue'
import { Collapse } from 'vue-collapsed'

const isExpanded = ref(false)
</script>

<template>
  <button @click="isExpanded = !isExpanded">This a panel.</button>
  <Collapse :when="isExpanded" class="v-collapse">
    <p>This is a paragraph.</p>
  </Collapse>
</template>

<style>
.v-collapse {
  transition: height 300ms cubic-bezier(0.33, 1, 0.68, 1);
}
</style>

Auto Duration

Vue Collapsed automatically calculates the optimal duration according to the content height. Use it by referencing the variable --vc-auto-duration:

.v-collapse {
  transition: height var(--vc-auto-duration) ease-out;
}

💡 Use calc() to control the speed, e.g. calc(var(--vc-auto-duration) * 0.75).

💡 Find a full list of easings at easings.net.

Additional transitions/styles

To transition other properties or add granular styles use the attribute data-collapse:

Transition From Enter Leave
Expand collapsed expanding expanded
Collapse expanded collapsing collapsed
.v-collapse {
  --dur-easing: var(--vc-auto-duration) cubic-bezier(0.33, 1, 0.68, 1);
  transition: height var(--dur-easing), opacity var(--dur-easing);
}

.v-collapse[data-collapse='expanded'],
.v-collapse[data-collapse='expanding'] {
  opacity: 1;
}

.v-collapse[data-collapse='collapsed'],
.v-collapse[data-collapse='collapsing'] {
  opacity: 0;
}

Above values can also be accessed using v-slot:

<Collapse :when="isExpanded" class="v-collapse" v-slot="{ state }">
  {{ state === 'collapsing' ? 'Collapsing...' : null }}
</Collapse>

Example - Accordion

<script setup>
import { reactive } from 'vue'
import { Collapse } from 'vue-collapsed'

const questions = reactive([
  {
    title: 'Question one',
    answer: 'Answer one',
    isExpanded: false // Initial value
  },
  {
    title: 'Question two',
    answer: 'Answer two',
    isExpanded: false
  },
  {
    title: 'Question three',
    answer: 'Answer three',
    isExpanded: false
  }
])

function handleAccordion(selectedIndex) {
  questions.forEach((_, index) => {
    questions[index].isExpanded = index === selectedIndex ? !questions[index].isExpanded : false
  })
}

/**
 * For individual control you might use:
 *
 * function handleMultiple(index) {
 *   questions[index].isExpanded = !questions[index].isExpanded
 * }
 */
</script>

<template>
  <div v-for="(question, index) in questions" :key="question.title">
    <button @click="handleAccordion(index)">
      {{ question.title }}
    </button>
    <Collapse :when="questions[index].isExpanded" class="v-collapse">
      <p>
        {{ question.answer }}
      </p>
    </Collapse>
  </div>
</template>

<style>
.v-collapse {
  transition: height var(--vc-auto-duration) cubic-bezier(0.33, 1, 0.68, 1);
}
</style>

Example - Callbacks

<script setup>
// ...

const sectionsRef = ref([])

function scrollIntoView(index) {
  sectionsRef.value[index].scrollIntoView({ behavior: 'smooth' })
}
</script>

<template>
  <div v-for="(question, index) in questions" :key="question.title" ref="sectionsRef">
    <button @click="handleAccordion(index)">
      {{ question.title }}
    </button>
    <Collapse
      :when="questions[index].isExpanded"
      :onExpanded="() => scrollIntoView(index)"
      class="v-collapse"
    >
      <p>
        {{ question.answer }}
      </p>
    </Collapse>
  </div>
</template>

<style>
.v-collapse {
  transition: height var(--vc-auto-duration) cubic-bezier(0.33, 1, 0.68, 1);
}
</style>

Make it accessible

<script setup>
import { ref, computed } from 'vue'
import { Collapse } from 'vue-collapsed'

const isExpanded = ref(false)

const toggleAttrs = computed(() => ({
  id: 'toggle-id',
  'aria-controls': 'collapse-id',
  'aria-expanded': isExpanded.value
}))

const collapseAttrs = {
  role: 'region',
  id: 'collapse-id',
  'aria-labelledby': 'toggle-id'
}

function handleCollapse() {
  isExpanded.value = !isExpanded.value
}
</script>

<template>
  <div>
    <button v-bind="toggleAttrs" @click="handleCollapse">This a panel.</button>
    <Collapse v-bind="collapseAttrs" :when="isExpanded" class="v-collapse">
      <p>This is a paragraph.</p>
    </Collapse>
  </div>
</template>

<style>
.v-collapse {
  transition: height var(--vc-auto-duration) cubic-bezier(0.33, 1, 0.68, 1);
}
</style>

Credits

@roginfarrer — For the sweetest implementation of a 0 to auto height transition using requestAnimationFrame I've ever seen.

License

MIT