✔︎ Last updated on November 6th, 2023
Do you want to learn how to create an FAQ accordion with VueJS?
Frequently Asked Questions (FAQ) accordion is a common feature on many websites. Frameworks like Bootstrap implement FAQ accordion but most of them use jQuery.
FAQ accordion is a simple enough task that can be implemented well with vanilla JavaScript but creating it with Vue is a great starter project for Vue learners.
In this tutorial, you will learn how to implement an FAQ accordion with VueJS in a few simple steps.
If you want to take a peek at what our final project will look like, here is a demo:
<h1>FAQ Accordion in VueJs</h1> <!-- Vue app --> <div id="app"> <faq-question v-for="(faqItem, index) in faqItems" :key="index" :faq="faqItem"> </faq-question> </div> <!-- added later --> <p> Wanna know I made this? Check out <a href="https://csswolf.com/how-to-create-a-faq-accordion-with-vuejs/"> this blog post. </a> </p>
/* basic styles */ * { box-sizing: border-box; font-family: system-ui; } body { padding: 30px 20px; max-width: 600px; width: 70%; margin: auto; } #app { font-family: system-ui; text-align: left; color: #2c3e50; font-size: 1em; padding: 20px 0px; } ::selection { user-select: none; } /* style the FAQ section */ .question { background: hsl(35 10% 30% / 0.1); text-transform: uppercase; cursor: pointer; font-weight: bold; box-shadow: 0px 4px 0px 0 #88888855; padding: 10px 0; transition: transform 0.2s; position: relative; } .question:hover { background: hsl(35 10% 30% / 0.15); } .question::before { content: "✅"; margin: 10px; } /* styles when the question is clicked */ .question:active { transform: translateY(4px); box-shadow: none; } .answer { transition: 0.25s; /* smooth slide-in */ height: 0; /* starts collapsed */ overflow: hidden; line-height: 1.5; } .answer::before { content: "👉"; margin-right: 10px; } /* style the toggleIcon */ .toggleIcon { font-size: 1.5em; font-weight: bold; position: absolute; right: 20px; display: inline-block; line-height: 0.5; color: #666; } a { color: blue; text-decoration: none; } a:hover { border-bottom: 1px solid blue; }
const app = Vue.createApp({ data() { return { faqItems: [ { q: "What is ChatGPT?", a: "ChatGPT is a language generation model developed by OpenAI. It's based on the GPT (Generative Pre-trained Transformer) architecture and is designed to generate human-like text and engage in natural-sounding conversations." }, { q: "How does ChatGPT work?", a: "ChatGPT uses a deep learning architecture called GPT, which processes input text and generates coherent and contextually relevant responses. It's trained on a vast amount of internet text to learn grammar, context, and style to generate responses that mimic human conversation." }, { q: "What can I use ChatGPT for?", a: "ChatGPT can be used for a variety of purposes, including drafting emails, brainstorming ideas, writing code, generating creative content, providing tutoring or information on a wide range of topics, and engaging in simulated conversations." } ] }; } }); app.component("faq-question", { template: ` <div class="faq"> <p class="question" @click="toggleAnswer"> {{ faq.q }} <span class="toggleIcon"> {{ this.isOpen ? "—" : "+" }} </span> </p> <p class="answer" ref="answer"> {{ faq.a }} </p> </div> `, // faq prop is passed down from app in each iteration props: ["faq"], data() { return { isOpen: false }; }, methods: { toggleAnswer() { // collapse if open, expand if not. if (this.isOpen) { this.collapse(); } else { this.expand(); } this.isOpen = !this.isOpen; }, collapse() { // select the answer element const answer = this.$refs.answer; answer.style.height = 0; // set its height to 0 }, expand() { // select answer element const answer = this.$refs.answer; // set its height to its normal scroll height to expand it fully answer.style.height = answer.scrollHeight + "px"; } } }); app.mount("#app");
Now, without wasting any time, let’s get started.
✅ ✏️💯 The following Vue concepts have been used in this article: v-for, v-if, props, $refs
, “handlebar syntax”, and components. If you are unfamiliar with any of these concepts, you can brush up on your Vue skills with some online classes.
Skeleton of the FAQ accordion
This is what our project looks like:
![How to Create an FAQ Accordion with VueJS 2 How to create an FAQ Accordion with VueJS.](https://csswolf.com/wp-content/uploads/2023/10/image-5.png)
- We have 3 sets of FAQs. The questions are given a class
.question
and the answers are given a class.answer
. - Initially, all the answers are hidden. Clicking on any question reveals slides down its answer.
- The toggle icon at the right end of every question flips between a big “+” and “-“, to indicate the visibility of the corresponding answer.
To build this app, I will use Vue 3 (options API) and code it in a Codepen, so if you want to follow along, open a new Codepen tab alongside this tutorial.
Laying things out
We will start with the basics. We need an div
element in our HTML to mount our Vue app. This is what we start with:
<h1> FAQ Accordion in VueJs </h1>
<div id="app"> </div>
We also need some CSS styles to make everything look good, so here are the styles:
/* basic styles */
* {
box-sizing: border-box;
}
body {
padding: 30px 20px;
max-width: 600px;
width: 70%;
margin: auto;
}
#app {
font-family: system-ui;
text-align: left;
color: #2c3e50;
font-size: 1em;
padding: 20px 0px;
}
::selection {
user-select: none;
}
/* style the FAQ section */
.question {
background: hsl(35 10% 30% / 0.1);
text-transform: uppercase;
cursor: pointer;
font-weight: bold;
box-shadow: 0px 4px 0px 0 #88888855;
padding: 10px 0;
transition: transform 0.2s;
position: relative;
}
.question:hover {
background: hsl(35 10% 30% / 0.15);
}
.question::before {
content: "✅";
margin: 10px;
}
/* styles when the question is clicked */
.question:active {
transform: translateY(4px);
box-shadow: none;
}
.answer {
overflow: hidden;
line-height: 1.5;
}
.answer::before {
content: "👉";
margin-right: 10px;
}
/* style the toggleIcon */
.toggleIcon {
font-size: 1.5em;
font-weight: bold;
position: absolute;
right: 20px;
display: inline-block;
line-height: 0.5;
color: #666;
}
This article isn’t about the CSS, so I won’t bore you with their explanations. You can just copy and paste them into your project.
Displaying the FAQs
Next, we come to the meat of the application, which is the Vue code. Let’s understand how we are going to approach this:
We have multiple FAQs to display, we can create a Vue component and then utilize the v-for
directive to generate them dynamically. To iterate over this component, we need an FAQ data array that stores the question-and-answer pairs.
Also read: CSS Margins: A Complete Guide
It is a good idea to store this array on the parent app and then pass the question-and-answer values as props to the child component. This is the JavaScript code for the parent app:
// app.js
const app = Vue.createApp({
data() {
return {
faqItems: [
{
q: "What is ChatGPT?",
a: "ChatGPT is a language generation model developed by OpenAI. It's based on the GPT (Generative Pre-trained Transformer) architecture and is designed to generate human-like text and engage in natural-sounding conversations."
},
{
q: "How does ChatGPT work?",
a:
"ChatGPT uses a deep learning architecture called GPT, which processes input text and generates coherent and contextually relevant responses. It's trained on a vast amount of internet text to learn grammar, context, and style to generate responses that mimic human conversation."
},
{
q: "What can I use ChatGPT for?",
a:
"ChatGPT can be used for a variety of purposes, including drafting emails, brainstorming ideas, writing code, generating creative content, providing tutoring or information on a wide range of topics, and engaging in simulated conversations."
},
// add more if required
]
};
},
});
This is the JavaScript code for the child component that we will attach to the app.js
(shown above). I call this component faq-question
.
This component has an HTML template that contains two paragraphs: one for the question and one for the answer. It also has a prop through which it receives an object (from its parent) containing a question and answer of an FAQ.
// app.js continued..
app.component("faq-question", {
template: `
<div class="faq">
<p class="question">
{{ faq.q }}
</p>
<p class="answer">
{{ faq.a }}
</p>
</div>
`,
// faq prop is passed down from app in each iteration
props: ["faq"],
});
app.mount("#app");
You won’t yet see anything on your screen, because we haven’t yet included this component in our HTML code. We need to update the HTML code above:
<h1>FAQ Accordion in VueJs</h1>
<div id="app">
<!-- FAQ child component -->
<faq-question
v-for="(faqItem, index) in faqItems"
:key="index"
:faq="faqItem"
></faq-question>
</div>
In the above code, the third attribute (:faq
) in the <faq-question>
tag is how a faqItem
object passed down to the child component.
Also read: CSS Padding: A Complete Guide
After writing this code, you will see the questions followed by their answers. But, at this point, this code doesn’t do anything. You can click on these questions, but they won’t reveal any answers. That’s because we haven’t yet made them listen to any events yet.
Let’s do that now.
Adding the click event listener
We will now add a click
event listener to the .question
in the faq-question
component and add some logic to toggle the visibility of the associated answer.
Adding an event listener is easy, just add a v-on
directive or its short-hand @
symbol.
// updated faq-question component
app.component("faq-question", {
template: `
<div class="faq">
<p class="question" @click="toggleAnswer">
{{ faq.q }}
</p>
<p class="answer">
{{ faq.a }}
</p>
</div>
`,
// faq prop is passed down from app in each iteration
props: ["faq"],
});
We have attached the toggleAnswer
method to the click event. Now we need to write logic so that clicking the question paragraph toggles the visibility of the answer.
There are two ways we can implement this functionality.
1. Toggling with v-if
One of the approaches is to simply add or remove the answer
element from the DOM tree. Here’s how we can go about it: we can maintain a data property called isOpen
which keeps track of the visibility of the answer. If the answer is hidden, isOpen
is false
otherwise, it’s true
.
We can then bind the v-if
directive to this property, which will render (or not render) the element based on the value of isOpen
data property.
Also read: How to Create a Radar Animation Effect with CSS
When a question is clicked, we simply toggle this data property. Here’s the updated code for the faq-question
component. (Updated lines are highlighted) —
// updated faq-question component
app.component("faq-question", {
template: `
<div class="faq">
<p class="question" @click="toggleAnswer">
{{ faq.q }}
</p>
<p class="answer" v-if="isOpen">
{{ faq.a }}
</p>
</div>
`,
// faq prop is passed down from app in each iteration
props: ["faq"],
data(){
return {
isOpen: false,
}
},
methods: {
toggleAnswer(){
this.isOpen = !this.isOpen;
}
}
});
Observe that as the isOpen
data property is set to false
, none of the answers are present in the DOM tree at this point. They will be added to the DOM only when a user clicks the question
. This is what it looks like:
This approach works but it’s a bit jarring. The answer element is appearing and disappearing all of a sudden at the click of a button.
It is happening because the v-if
directive in Vue is adding or removing the element from the DOM tree. As the answer
node is being added and removed, we cannot apply any transition effect to make its appearance & disappearance smooth.
We can however take a different approach and make the answer element slide down (with a duration), so the experience is smooth for the user.
You will need to undo the changes made in this section before trying out the next approach.
2. Adding the slide-down effect
To make the slide-down effect, we need the element to be present in the DOM tree, but keep it collapsed until its sibling question
is clicked.
To keep the answer
collapsed, we can set its height
to 0
, overflow: hidden
and transition : 0.25s
. We need to add these lines of CSS code:
.answer {
transition: 0.25s; /* smooth slide-in */
height: 0; /* starts collapsed */
overflow: hidden; /* to prevent spillage */
}
We will again keep track of the status with the help of a data property isOpen, but this time, we won’t attach it to the v-if directive. Instead, with its help, we will toggle the height
of the answer
element whenever the question
is clicked.
Here’s the updated code for the faq-question
component:
app.component("faq-question", {
template: `
<div class="faq">
<p class="question" @click="toggleAnswer">
{{ faq.q }}
</p>
<p class="answer" ref="answer">
{{ faq.a }}
</p>
</div>
`,
// faq prop is passed down from app in each iteration
props: ["faq"],
data() {
return {
isOpen: false
};
},
methods: {
toggleAnswer() {
if (this.isOpen) {
this.collapse();
} else {
this.expand();
}
// update the value of the data property
this.isOpen = !this.isOpen;
},
collapse() {
// select the answer element
const answer = this.$refs.answer;
answer.style.height = 0;
},
expand() {
// select answer element
const answer = this.$refs.answer;
// set its height to its normal scroll height to expand it fully
answer.style.height = answer.scrollHeight + "px";
}
}
});
Here’s an explanation of these changes:
In line 7:
- we have added a new attribute called
ref
. This attribute (provided by Vue) can be used to give individual identity to any element. It works kind of likedocument.getElementByID("#id")
provided by JavaScript. - We have given an identity of
answer
to this element. We can then access this element with the help of the$refs
array. We need access to this element because we will change its CSSheight
through JavaScript.
In lines 19 to 39:
- We have created 3 methods:
toggleAnswer
,collapse
andexpand
. ThetoggleAnswer
simply calls one of the other methods depending on the value ofisOpen
. It also toggles the value of theisOpen
data property. - In the
collapse
andexpand
methods, we first select theanswer
element through$refs
array and then update its height.
Here’s a working Codepen example demonstrating the code so far:
<h1>FAQ Accordion in VueJs</h1> <div id="app"> <!-- This is where Vue will inject code. --> <faq-question v-for="(faqItem, index) in faqItems" :key="index" :faq="faqItem"> </faq-question> </div>
/* basic styles */ * { box-sizing: border-box; } body { padding: 30px 20px; max-width: 600px; width: 70%; margin: auto; } #app { font-family: system-ui; text-align: left; color: #2c3e50; font-size: 1em; padding: 20px 0px; } ::selection { user-select: none; } /* style the FAQ section */ .question { background: hsl(35 10% 30% / 0.1); text-transform: uppercase; cursor: pointer; font-weight: bold; box-shadow: 0px 4px 0px 0 #88888855; padding: 10px 0; transition: transform 0.2s; position: relative; } .question:hover { background: hsl(35 10% 30% / 0.15); } .question::before { content: "✅"; margin: 10px; } /* styles when the question is clicked */ .question:active { transform: translateY(4px); box-shadow: none; } .answer { transition: 0.25s; /* smooth slide-in */ height: 0; /* starts collapsed */ overflow: hidden; line-height: 1.5; } .answer::before { content: "👉"; margin-right: 10px; } /* style the toggleIcon */ .toggleIcon { font-size: 1.5em; font-weight: bold; position: absolute; right: 20px; display: inline-block; line-height: 0.5; color: #666; }
const app = Vue.createApp({ data() { return { faqItems: [ { q: "What is ChatGPT?", a: "ChatGPT is a language generation model developed by OpenAI. It's based on the GPT (Generative Pre-trained Transformer) architecture and is designed to generate human-like text and engage in natural-sounding conversations." }, { q: "How does ChatGPT work?", a: "ChatGPT uses a deep learning architecture called GPT, which processes input text and generates coherent and contextually relevant responses. It's trained on a vast amount of internet text to learn grammar, context, and style to generate responses that mimic human conversation." }, { q: "What can I use ChatGPT for?", a: "ChatGPT can be used for a variety of purposes, including drafting emails, brainstorming ideas, writing code, generating creative content, providing tutoring or information on a wide range of topics, and engaging in simulated conversations." } ] }; } }); app.component("faq-question", { template: ` <div class="faq"> <p class="question" @click="toggleAnswer"> {{ faq.q }} </p> <p class="answer" ref="answer"> {{ faq.a }} </p> </div> `, // faq prop is passed down from app in each iteration props: ["faq"], data() { return { isOpen: false }; }, methods: { toggleAnswer() { if (this.isOpen) { this.collapse(); } else { this.expand(); } this.isOpen = !this.isOpen; }, collapse() { // select the answer element const answer = this.$refs.answer; answer.style.height = 0; }, expand() { // select answer element const answer = this.$refs.answer; // set its height to its normal scroll height to expand it fully answer.style.height = answer.scrollHeight + "px"; } } }); app.mount("#app");
At this point, you have a working FAQ accordion, with a smooth transition. There’s just one thing left to do: adding a toggleIcon
to the right side of every .question
. This icon will switch between +
and -
at every click.
Adding a toggleIcon
Adding a toggleIcon
is simple. We just have to render an HTML element whose value will toggle with the isOpen
property. Here’s the updated template of the faq-question
component:
// app.js code above 👆
// faq-question's template updated
app.component("faq-question", {
template: `
<div class="faq">
<p class="question" @click="toggleAnswer">
{{ faq.q }}
<span class="toggleIcon">
{{ this.isOpen ? "—" : "+" }}
</span>
</p>
<p class="answer" ref="answer">
{{ faq.a }}
</p>
</div>
`,
// remaining faq-question component's code here 👇
You don’t have to worry about styling it with CSS. We have already provided the necessary CSS declarations in our CSS code at the beginning of this article.
Here’s the final working code:
<h1>FAQ Accordion in VueJs</h1> <div id="app"> <!-- This is where Vue will inject code. --> <faq-question v-for="(faqItem, index) in faqItems" :key="index" :faq="faqItem"> </faq-question> </div>
/* basic styles */ * { box-sizing: border-box; font-family: system-ui; } body { padding: 30px 20px; max-width: 600px; width: 70%; margin: auto; } #app { font-family: system-ui; text-align: left; color: #2c3e50; font-size: 1em; padding: 20px 0px; } ::selection { user-select: none; } /* style the FAQ section */ .question { background: hsl(35 10% 30% / 0.1); text-transform: uppercase; cursor: pointer; font-weight: bold; box-shadow: 0px 4px 0px 0 #88888855; padding: 10px 0; transition: transform 0.2s; position: relative; } .question:hover { background: hsl(35 10% 30% / 0.15); } .question::before { content: "✅"; margin: 10px; } /* styles when the question is clicked */ .question:active { transform: translateY(4px); box-shadow: none; } .answer { transition: 0.25s; /* smooth slide-in */ height: 0; /* starts collapsed */ overflow: hidden; line-height: 1.5; } .answer::before { content: "👉"; margin-right: 10px; } /* style the toggleIcon */ .toggleIcon { font-size: 1.5em; font-weight: bold; position: absolute; right: 20px; display: inline-block; line-height: 0.5; color: #666; }
const app = Vue.createApp({ data() { return { faqItems: [ { q: "What is ChatGPT?", a: "ChatGPT is a language generation model developed by OpenAI. It's based on the GPT (Generative Pre-trained Transformer) architecture and is designed to generate human-like text and engage in natural-sounding conversations." }, { q: "How does ChatGPT work?", a: "ChatGPT uses a deep learning architecture called GPT, which processes input text and generates coherent and contextually relevant responses. It's trained on a vast amount of internet text to learn grammar, context, and style to generate responses that mimic human conversation." }, { q: "What can I use ChatGPT for?", a: "ChatGPT can be used for a variety of purposes, including drafting emails, brainstorming ideas, writing code, generating creative content, providing tutoring or information on a wide range of topics, and engaging in simulated conversations." } ] }; } }); app.component("faq-question", { template: ` <div class="faq"> <p class="question" @click="toggleAnswer"> {{ faq.q }} <span class="toggleIcon"> {{ this.isOpen ? "—" : "+" }} </span> </p> <p class="answer" ref="answer"> {{ faq.a }} </p> </div> `, // faq prop is passed down from app in each iteration props: ["faq"], data() { return { isOpen: false }; }, methods: { toggleAnswer() { // collapse if open, expand if not. if (this.isOpen) { this.collapse(); } else { this.expand(); } this.isOpen = !this.isOpen; }, collapse() { // select the answer element const answer = this.$refs.answer; answer.style.height = 0; // set its height to 0 }, expand() { // select answer element const answer = this.$refs.answer; // set its height to its normal scroll height to expand it fully answer.style.height = answer.scrollHeight + "px"; } } }); app.mount("#app");
Analyze the code and play around with it.
Final words
Creating an FAQ accordion in VueJs is pretty easy if you understand a few basic concepts. It’s amazing to see how much you can accomplish with how little knowledge of Vue.
We used only a few basic Vue-specific directives and elements in this code, namely v-for
, $refs
, Vue component and its template.
If you learn the concepts used in this article, you can already build some awesome projects.
If you loved (💘) this article, please (🙏) share it with your friends.