Vue Collapsed

December 30, 2025 ยท View on GitHub

npm dependencies npm bundle size downloads GitHub Workflow Status

Vue Collapsed

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

Examples and Demo - Stackblitz

Installation

npm i vue-collapsed

Usage

<script setup>
import { ref } from 'vue'

import { Collapse } from 'vue-collapsed'

const isExpanded = ref(false)
</script>

<template>
  <button @click="isExpanded = !isExpanded">Toggle</button>

  <Collapse :when="isExpanded">
    <p>{{ 'Collapsed '.repeat(100) }}</p>
  </Collapse>
</template>

Props

NameTypeDescriptionRequired
whenbooleanControls the collapse/expand state:white_check_mark:
baseHeightnumberCollapsed height in px. Defaults to 0.:x:
askeyof HTMLElementTagNameMapTag to use instead of div:x:

Emits

NameTypeDescription
@expand() => voidEmitted when expansion starts
@expanded() => voidEmitted when expansion completes
@collapse() => voidEmitted when collapse starts
@collapsed() => voidEmitted when collapse completes

Automatic transition (default)

By default, the following transition is always added to the Collapse element:

transition: height var(--vc-auto-duration) cubic-bezier(0.33, 1, 0.68, 1);

--vc-auto-duration is calculated dynamically and corresponds to the optimal transition duration based on the element's height.

Custom transition

To use a custom duration or easing, add a class to the Collapse component that transitions the height property:

.collapsed-area {
  transition: height 300ms ease-out;
}
<Collapse :when="isExpanded" class="collapsed-area">
  <p>{{ 'Collapsed '.repeat(100) }}</p>
</Collapse>

Multiple transitions

To transition other properties, use the data-collapse attribute:

TransitionFromEnterLeave
Expandcollapsedexpandingexpanded
Collapseexpandedcollapsingcollapsed
.collapsed-area {
  --transition-base: 300ms cubic-bezier(0.33, 1, 0.68, 1);

  transition:
    height var(--transition-base),
    opacity var(--transition-base);
}

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

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

Alternatively, to use different easings or durations for expanding and collapsing:

.collapsed-area[data-collapse='expanding'] {
  transition: height 600ms ease-in-out;
}

.collapsed-area[data-collapse='collapsing'] {
  transition: height 300ms ease-out;
}

The values of the data-collapse attribute can be accessed using v-slot:

<Collapse :when="isExpanded" 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 onQuestionToggle(toggleIndex) {
  questions.forEach((_, i) => {
    questions[i].isExpanded = i === toggleIndex ? !questions[i].isExpanded : false
  })
}
</script>

<template>
  <div v-for="(q, i) in questions" :key="q.title">
    <button @click="onQuestionToggle(i)">
      {{ q.title }}
    </button>
    <Collapse :when="q.isExpanded">
      <p>
        {{ q.answer }}
      </p>
    </Collapse>
  </div>
</template>

Accessibility

vue-collapsed automatically detects if users prefer reduced motion and disables transitions accordingly, while maintaining the same API behavior (emitting events and applying post-transition styles).

You should add aria attributes to the Collapse element based on your specific use case.

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

const isExpanded = ref(false)

const TOGGLE_ID = useId()
const COLLAPSE_ID = useId()

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">Toggle panel</button>
    <Collapse v-bind="collapseAttrs" :when="isExpanded">
      <p>{{ 'Collapsed '.repeat(100) }}</p>
    </Collapse>
  </div>
</template>

Manually disabling transitions

<template>
  <Collapse :when="isExpanded" class="collapsed-area">
    <p>{{ 'Collapsed '.repeat(100) }}</p>
  </Collapse>
</template>

<style>
.collapsed-area {
  transition: none;
}
</style>

License

MIT