Dynamic CSS height transition from any to auto and vice versa. Accordion ready.
Examples and Demo - Stackblitz
💡 Requires Vue v3.0.0 or above.
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'
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 | ❌ |
<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>
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.
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>
<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>
<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>
<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>
@roginfarrer — For the sweetest implementation of a 0 to auto height transition using requestAnimationFrame I've ever seen.
MIT