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
Type | Path / Version | Purpose | Optional |
---|---|---|---|
Vue version | Vue 3 | Composition API | No |
Styles | ../../assets/main.css | CSS Variables | Yes |
Functions | ../../use/uuid | Assign ids to items | No |
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.
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est asperiores repellat.
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.
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est asperiores repellat.
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
At vero eos et accusamus et iusto odio dignissimos ducimus qui ...
At vero eos et accusamus et iusto odio dignissimos ducimus qui ...
At vero eos et accusamus et iusto odio dignissimos ducimus qui ...
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
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.
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est asperiores repellat.
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>