QAccordion component

Accordions are useful when toggling a large amount of content. Radunia's implementation uses no Javascript to calculate the height of the item's content, but a max-height transition, together with overflow-hidden. It adds accessibility by binding :aria-expanded="isExpanded" to each created child element and makes use of the composition API.

Requirements

TypePath / VersionPurposeOptional
Vue versionVue 3Composition APINo
Styles../../assets/main.cssCSS VariablesYes
Functions../../use/uuidAssign ids to itemsNo

Usage

Import the following component/s:

import QAccordion from '../../components/UI/Accordion/QAccordion.vue'
import QAccordionItem from '../../components/UI/Accordion/QAccordionItem.vue'

Single element usage

Each accordion item can be used independently from one another. It handles toggling internally by assigning unique ids to each element.

Example

<QAccordionItem title="Item one">
  <p>
    At vero eos et accusamus et iusto odio dignissimos ducimus qui
    blanditiis praesentium voluptatum ...
  </p>
</QAccordionItem>

Rounded borders

Whether used standalone or in groups, this component usually looks better with rounded borders.

Example

<QAccordionItem title="Item one" :roundedTop="true" :roundedBottom="true">
  <p>
    At vero eos et accusamus et iusto odio dignissimos ducimus qui
    blanditiis praesentium voluptatum ...
  </p>
</QAccordionItem>

Multiple element usage

Accordion items stack on one another by default. You can combine several single items to form a bigger accordion item.

Add rounded borders

Use the roundedTop and roundedBottom attributes on the first and last item

Example

Example

 





 



<QAccordionItem title="Item one" :roundedTop="true">
  <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui ...</p>
</QAccordionItem>
<QAccordionItem title="Item two">
  <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui ...</p>
</QAccordionItem>
<QAccordionItem title="Item three" :roundedBottom="true">
  <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui ...</p>
</QAccordionItem>

Wrapping multiple elements

Instead of styling elements individually, you can place them inside a wrapper element

Accordion wrapper title

Example

 








<QAccordion header="Accordion wrapper title">
  <QAccordionItem title="Item one">
  </QAccordionItem>
  <QAccordionItem title="Item two">
  </QAccordionItem>
  <QAccordionItem title="Item three">
  </QAccordionItem>
</QAccordion>

Custom icon

You can replace the standard '+' sign with your own icon and adjust its rotation behavior.

Example

 






<QAccordionItem title="Item one" icon="" :baseRotation="0" :targetRotation="90">
<p>
  At vero eos et accusamus et iusto odio dignissimos ducimus qui
  blanditiis praesentium voluptatum ...
</p>
</QAccordionItem>

Full component's code

QAccordion

<template>
  <div class="q-accordion-wrapper">
    <header class="q-accordion-header">{{ header }}</header>
    <slot />
    <footer class="q-accordion-footer">
      <slot name="accordion-footer" />
    </footer>
  </div>
</template>

<script>
export default {
  props: {
    header: {
      type: String,
      required: false,
    },
  },
};
</script>

<style>
.q-accordion-wrapper {
  color: var(--text-color-primary);
  width: 100%;
}

.q-accordion-header,
.q-accordion-footer {
  background-color: var(--background-color-secondary);
  color: var(--text-color-primary);
  text-align: left;
  border: none;
}

.q-accordion-header {
  border-top-left-radius: var(--gap-xs);
  border-top-right-radius: var(--gap-xs);
  font-size: var(--text-size-xxl);
  padding: var(--gap-sm) var(--gap-lg);
}

.q-accordion-footer {
  border-bottom-left-radius: var(--gap-xs);
  border-bottom-right-radius: var(--gap-xs);
  padding: var(--gap-sm) var(--gap-lg);
}
</style>

QAccordionItem

<template>
  <div :class="{ reverse: reversed, topside: !reversed }">
    <button
      type="button"
      :class="{
        expanded: isExpanded,
        collapsed: !isExpanded,
        'q-rounded-top': roundedTop,
        'q-rounded-bottom': roundedBottom && !isExpanded,
      }"
      @click="isExpanded = !isExpanded"
      class="q-accordion-header-wrapper"
    >
      <h3 class="q-accordion-header-title">
        {{ title }}
      </h3>
      <span class="q-accordion-header-icon" :id="iconId">
        {{ icon }}
      </span>
    </button>
    <section
      :id="panelId"
      :class="{ 'q-rounded-bottom': roundedBottom && isExpanded }"
      :aria-expanded="isExpanded"
      class="q-accordion-body"
    >
      <slot />
    </section>
  </div>
</template>

<script>
import { ref, watch, onMounted } from "vue";
import uuid from "../../../use/uuid";

export default {
  setup({ expanded, targetRotation, baseRotation }) {
    const panelId = `panel-${uuid()}`;
    const iconId = `icon-${uuid()}`;
    let isExpanded = ref(expanded);

    // Rotate the icon and open/close the accordion on click
    const togglePanel = () => {
      const panel = document.getElementById(panelId);
      const icon = document.getElementById(iconId);

      if (isExpanded.value === true) {
        panel.style.maxHeight = panel.scrollHeight + "px";
        icon.style.transform = `rotate(${targetRotation}deg)`;
      } else {
        panel.style.maxHeight = null;
        icon.style.transform = `rotate(${baseRotation}deg)`;
      }
    };

    watch(isExpanded, () => togglePanel());
    onMounted(() => togglePanel());

    return { isExpanded, panelId, iconId };
  },
  props: {
    // Content
    title: {
      type: String,
      required: true,
    },

    icon: {
      type: String,
      required: false,
      default: "+",
    },

    // Style properties
    targetRotation: {
      type: Number,
      required: false,
      default: 45,
    },

    baseRotation: {
      type: Number,
      required: false,
      default: 0,
    },

    reversed: {
      type: Boolean,
      required: false,
      default: false,
    },

    roundedTop: {
      type: Boolean,
      required: false,
      default: false,
    },

    roundedBottom: {
      type: Boolean,
      required: false,
      default: false,
    },

    expanded: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
};
</script>

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

.reverse {
  display: flex;
  flex-direction: column-reverse;
}

.q-accordion-header-wrapper {
  width: 100%;
  background-color: var(--background-color-secondary);
  padding: var(--gap-lg);
  border: none;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
}

.q-accordion-header-title,
.q-accordion-header-icon {
  font-size: var(--text-size-lg);
  color: var(--text-color-primary);
  margin: 0;
}

.q-accordion-header-icon {
  transition: all var(--duration-quickest);
  text-align: right;
}

.q-accordion-header-title {
  text-align: left;
}

.q-rounded-top {
  border-top-left-radius: var(--gap-xs);
  border-top-right-radius: var(--gap-xs);
}

.q-rounded-bottom {
  border-bottom-left-radius: var(--gap-xs);
  border-bottom-right-radius: var(--gap-xs);
}

.q-accordion-body {
  background-color: var(--background-color-tartiary);
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--duration-quickest) ease-in-out;
  padding: 0 var(--gap-lg);
}
</style>