Skip to content

Accordion

Accordion Example

The accordion presented here is made up of two components. There is an outer accordion component that acts as a container, with accordion-panel components as children that can be expanded or collapsed.

Usage might look something like this:

vue
<template>
  <basic-accordion>
    <basic-accordion-panel title="First">
      First panel content
    </basic-accordion-panel>
    <basic-accordion-panel title="Second">
      <template v-for="i in 20">
        Second panel content {{ i }}
        <br v-if="i !== 20">
      </template>
    </basic-accordion-panel>
    <basic-accordion-panel title="Third">
      Third panel content
    </basic-accordion-panel>
  </basic-accordion>
</template>

<script setup>
import BasicAccordion from './accordion.vue'
import BasicAccordionPanel from './accordion-panel.vue'
</script>
<template>
  <basic-accordion>
    <basic-accordion-panel title="First">
      First panel content
    </basic-accordion-panel>
    <basic-accordion-panel title="Second">
      <template v-for="i in 20">
        Second panel content {{ i }}
        <br v-if="i !== 20">
      </template>
    </basic-accordion-panel>
    <basic-accordion-panel title="Third">
      Third panel content
    </basic-accordion-panel>
  </basic-accordion>
</template>

<script setup>
import BasicAccordion from './accordion.vue'
import BasicAccordionPanel from './accordion-panel.vue'
</script>

While the 3 children given in this example are static, they could also be created dynamically using v-for over a suitable array.

Running this example we get:

Live Example
First
Second
Third

SFC Playground

The code for accordion.vue:

vue
<template>
  <div class="accordion"><slot /></div>
</template>

<script setup>
import { computed, provide, ref } from 'vue'

// Holds the id of the currently expanded panel
const expanded = ref(null)

// Use `provide` to communicate with the child panels
provide('accordion-register', () => {
  const id = Symbol()

  return {
    expanded: computed(() => expanded.value === id),

    toggle () {
      expanded.value = expanded.value === id ? null: id
    },

    unregister () {
      if (expanded.value === id) {
        expanded.value = null
      }
    }
  }
})
</script>

<style scoped>
.accordion {
  border: 1px solid #ccc;
  display: flex;
  flex-direction: column;
  height: 300px;
  width: 240px;
}
</style>
<template>
  <div class="accordion"><slot /></div>
</template>

<script setup>
import { computed, provide, ref } from 'vue'

// Holds the id of the currently expanded panel
const expanded = ref(null)

// Use `provide` to communicate with the child panels
provide('accordion-register', () => {
  const id = Symbol()

  return {
    expanded: computed(() => expanded.value === id),

    toggle () {
      expanded.value = expanded.value === id ? null: id
    },

    unregister () {
      if (expanded.value === id) {
        expanded.value = null
      }
    }
  }
})
</script>

<style scoped>
.accordion {
  border: 1px solid #ccc;
  display: flex;
  flex-direction: column;
  height: 300px;
  width: 240px;
}
</style>

The corresponding accordion-panel.vue is:

vue
<template>
  <div class="accordion-panel" :class="{ expanded }">
    <div class="header" @click="toggle">{{ title }}</div>
    <div v-if="expanded" class="body"><slot /></div>
  </div>
</template>

<script setup>
import { inject, onUnmounted } from 'vue'

const props = defineProps({
  title: {
    required: true,
    type: String
  }
})

const register = inject('accordion-register')

const { expanded, toggle, unregister } = register()

onUnmounted(unregister)
</script>

<style scoped>
.accordion-panel {
  display: flex;
  flex-direction: column;
}

.accordion-panel + .accordion-panel {
  margin-top: 1px;
}

.header {
  background-color: #e6f6ff;
  border: 1px solid #ccc;
  cursor: pointer;
  margin: -1px;
  padding: 5px;
}

.body {
  background-color: #f7fcff;
  border: 1px solid #ccc;
  flex: auto;
  margin: 0 -1px -1px;
  overflow: auto;
}

.expanded {
  flex: auto;
  min-height: 0;
}
</style>
<template>
  <div class="accordion-panel" :class="{ expanded }">
    <div class="header" @click="toggle">{{ title }}</div>
    <div v-if="expanded" class="body"><slot /></div>
  </div>
</template>

<script setup>
import { inject, onUnmounted } from 'vue'

const props = defineProps({
  title: {
    required: true,
    type: String
  }
})

const register = inject('accordion-register')

const { expanded, toggle, unregister } = register()

onUnmounted(unregister)
</script>

<style scoped>
.accordion-panel {
  display: flex;
  flex-direction: column;
}

.accordion-panel + .accordion-panel {
  margin-top: 1px;
}

.header {
  background-color: #e6f6ff;
  border: 1px solid #ccc;
  cursor: pointer;
  margin: -1px;
  padding: 5px;
}

.body {
  background-color: #f7fcff;
  border: 1px solid #ccc;
  flex: auto;
  margin: 0 -1px -1px;
  overflow: auto;
}

.expanded {
  flex: auto;
  min-height: 0;
}
</style>

Vue Patterns

See Coupled Components with provide/inject.

Libraries

Various libraries include an accordion component, or a component that can achieve a similar effect. These include: