Accordion in Vue

owais11-art

Owais Khan

Posted on February 19, 2023

Accordion in Vue

Accordion

In this article we will be creating an accordion with vue.js. An accordion is used in websites mostly for FAQ section where answer section expands or collapses when user clicks on question itself or any icon like chevron🔰, arrow➡️ or plus sign➕.

Preview: live Demo

First, we will create vue project using vite. So, ensure that vite and node is already installed on your computer. After installing vite type following command in command prompt or terminal and press Enter:

    npm init vite <project-name>
Enter fullscreen mode Exit fullscreen mode

After pressing Enter you will be prompted with few questions like which framework you want to use vite with we will choose vue and which language JavaScript or TypeScript we will choose JavaScript.

After answering above questions vite will create a vue project for us. After that we have to run few commands in terminal:

    cd <project-name>
    npm install // It will install all the necessary dependencies for our project in node_modules folder 
Enter fullscreen mode Exit fullscreen mode

We will be using LESS as css-preprocessor. So, lets install it.

    npm install -D less // installs less as dev-dependency
Enter fullscreen mode Exit fullscreen mode

Once vite creates project for us we can run our project using command:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

Above command will start development server created by vite at http://localhost:5173/ and you will see a welcome page at this address.

Now, lets modify the project as per our need. First, delete HelloWorld component from src/components folder then boilerplate code inside src/App.vue file and also delete style.css file from src folder.
As we are going to use LESS as css-preprocessor, create a folder less and inside of the folder create global.less file and add this code in it:

    *{
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    body{
        font-family: 'Source Sans Pro', sans-serif;
        background-color: antiquewhite;
    }
Enter fullscreen mode Exit fullscreen mode

In above code we are resetting the default browser styles and adding some general styles to body.

Open src/main.js and you will see something like this:

    import { createApp } from 'vue'
    import './style.css'
    import App from './App.vue'

    createApp(App).mount('#app')
Enter fullscreen mode Exit fullscreen mode

Replace ./style.css import with ./less/global.less because we deleted style.css file and created less/global.less file. After replacing main.js will look like this:

    import { createApp } from 'vue'
    import './less/global.less'
    import App from './App.vue'

    createApp(App).mount('#app')
Enter fullscreen mode Exit fullscreen mode

Creating Accordion

First we will create Accordion.vue component in src/components folder and import that component in to our App.vue component.

Accordion.vue👇🏻

    <script setup></script>
    <template>
        <h1>Accordion</h1>
    </template>
    <style scoped lang="less"></style>
Enter fullscreen mode Exit fullscreen mode

App.vue👇🏻

    <script setup>
        /* importing Accordion.vue👇🏻 */
        import Accordion from './components/Accordion'
    </script>
    <template>
        <Accordion/> <!-- 👈🏻Using Accordion.vue -->
    </template>
Enter fullscreen mode Exit fullscreen mode

App.vue is the parent components of all other components in our app. This component is passed to createApp function provided by vue.js which translates our vue code into native JavaScript, HTML and CSS and then the translated code is passed to mount function which puts this translated code into an html element with id app inside index.html file which is then executed by browser. It is important to import other components in App.vue so that vue can translate the code in all of our components.

In src/components folder we created Accordion.vue component which houses all other components for our app. Currently, this component only contains a h1 tag with text 'Accordion'. So, if we run npm run dev in terminal and open browser we will see 'Accordion' text on our screen.

Now, let's create a main container for our accordion and the container is simply a html main element. And also give some styling to it. Inside main we will create a div with class faqs which will contain all of our accordion items.

    <script setup></script>
        <template>
            <main>  <!-- 👈🏻Main container for accordion -->
                <div class="faqs"></div>
            </main>
        </template>
    <style scoped lang="less">
        .main{
            width: 500px;
            box-shadow: 5px 5px 20px 0 rgba(0, 0, 0, 0.5);
            margin: 0 auto;
            margin-top: 10%;
            margin-bottom: 10%;
            background-color: rgb(240, 248, 255);
            padding: 10px;
            border-radius: 6px;
            .faqs{
                display: flex;
                flex-direction: column;
                gap: 20px;
            }
        }
    </style>
Enter fullscreen mode Exit fullscreen mode

Now, we will create another component in src/component folder called Faq.vue in which we will write code for single accordion item. Our single accordion item will have a div element with class faq which contains two div elements with classes header and answer respectively. In header element we will create further two div elements with classes question and icon. question div will contain a faq-question and icon div contains a chevron image. And in answer element we have a p element which contains faq-answer.

Faq.vue👇🏻

    <script setup></script>
    <template>
        <div class="faq">
            <div class="header">
                <div class="question">
                    <!-- contains question -->
                </div>
                <div class="icon">
                    <img src="" alt=""/>
                </div>
            </div>
            <div class="answer">
                <p> <!-- contains answer --> </p>
            </div>
        </div>
    </template>
    <style scoped lang="less"></style>
Enter fullscreen mode Exit fullscreen mode

Our accordion will have five questions in total and we will store those in an array of objects. Each object will have four properties:

  • id an unique number for identifying an object.
  • question stores faq question.
  • answer stores faq answer.
  • isOpen stores a boolean value which specifies whether answer part of accordion is expanded or collapsed.
    const data = [
        {
            "id": 1,
            "question": "What is the capital of Australia?",
            "answer": "The capital of Australia is Canberra. It is a relatively new city, established in 1913, and is located between Sydney and Melbourne. Canberra is home to numerous national institutions and landmarks, including Parliament House, the Australian War Memorial, and the National Gallery of Australia. The city is known for its modern architecture and urban planning, and has a population of over 400,000 people. Despite not being one of the country's largest cities, Canberra is an important political and cultural center, and has a significant impact on the nation's economy and development.",
            "isOpen": false
        },
        {
            "id": 2,
            "question": "What is the tallest animal on earth?",
            "answer": "The tallest animal on earth is the giraffe, which can grow up to 18 feet tall. Giraffes are known for their long necks, which can reach up to 6 feet in length, and are used to reach leaves and fruits from tall trees. Giraffes are found in savannas and grasslands in Africa, and are herbivorous, feeding on leaves, fruits, and flowers. Despite their size, giraffes are social animals and live in groups called towers. They are also known for their distinctive spotted coat, which helps them blend in with their environment and avoid predators.",
            "isOpen": false
        },
        {
            "id": 3,
            "question": "What is the largest country in the world by area?",
            "answer": "The largest country in the world by area is Russia, which covers over 17 million square kilometers. Russia is located in northern Eurasia, and is bordered by Norway, Finland, Estonia, Latvia, Lithuania, Poland, Belarus, Ukraine, Georgia, Azerbaijan, Kazakhstan, China, North Korea, and Mongolia. Russia has a population of over 144 million people, and is known for its rich history and culture, as well as its natural resources, such as oil, gas, and minerals. The country is also home to numerous landmarks and tourist attractions, including the Red Square, the Kremlin, and the Hermitage Museum.",
            "isOpen": false
        },
        {
            "id": 4,
            "question": "What is the largest animal on earth?",
            "answer": "The largest animal on earth is the blue whale, which can grow up to 100 feet in length and weigh up to 200 tons. Blue whales are found in all the world's oceans, and are known for their distinctive blue-gray coloration and long, slender bodies. They are filter feeders, feeding on tiny shrimp-like creatures called krill, and can consume up to 4 tons of krill in a single day. Despite their enormous size, blue whales are graceful swimmers and can travel at speeds of up to 30 miles per hour.",
            "isOpen": false
        },
        {
            "id": 5,
            "question": "What is the capital of France?",
            "answer": "The capital of France is Paris. It is one of the most famous cities in the world, known for its beautiful architecture, rich history, and world-class museums and landmarks. Paris is home to iconic landmarks such as the Eiffel Tower, the Louvre Museum, and Notre-Dame Cathedral, and is famous for its cuisine and fashion. The city has a population of over 2 million people, and is a global center for art, fashion, and culture. Despite its cosmopolitan character, Paris has retained much of its historic charm, with narrow streets",
            "isOpen": false
        }
  ]
Enter fullscreen mode Exit fullscreen mode

In Accordion.vue we will store above array as a reactive state using ref so import ref from vue. Currently, our questions and answers are in an array but to display them on browser we have to extract those from array and put them into html elements. We have already created a component for that which is Faq.vue so import it. Faq.vue is a component for single accordion or single question & answer so, we have to pass a single object from data array to the component at a time. We have total five questions & answers which means we have to call Faq.vue five times and pass a single object of question & answer each time. Calling Faq.vue five times is tedious so we will us v-for directive on Faq.vue to loop over data array and in each iteration we will pass single faq object to the component.

    <script setup>
        import { ref } from 'vue'
        import Faq from './Faq'

        const faqs = ref(data) // data is above array of faq questions & answers
    </script>
        <template>
            <main>  <!-- 👈🏻Main container for accordion -->
                <div class="faqs">
                    <!-- Looping over faqs array and passing single faq as a prop -->
                    <Faq
                        v-for="faq in faqs"
                        :key="faq.id"
                        :faq="faq"
                    />
                </div>
            </main>
        </template>
    <style scoped lang="less">
        .main{
            width: 500px;
            box-shadow: 5px 5px 20px 0 rgba(0, 0, 0, 0.5);
            margin: 0 auto;
            margin-top: 10%;
            margin-bottom: 10%;
            background-color: rgb(240, 248, 255);
            padding: 10px;
            border-radius: 6px;
            .faqs{
                display: flex;
                flex-direction: column;
                gap: 20px;
            }
        }
    </style>
Enter fullscreen mode Exit fullscreen mode

After passing faq as a prop we have to recieve that inside of Faq.vue using defineProps macro. Then we can use faq object in template of Faq.vue to extract question & answer from it. We also need a chevron icon in each accordion so, let's import that and bind the src attribute of img element to it.
At the end lets add some styling to the component.

    <script setup>
        import chevron from '../assets/chevron.svg'
        defineProps(['faq']) // recieving faq object as a prop.
    </script>
    <template>
        <div class="faq">
            <div class="header">
                <div class="question">
                    {{ faq.question }} <!--Extracting question from faq prop -->
                </div>
                <div class="icon">
                    <img :src="chevron" alt="chevron-icon"/>
                </div>
            </div>
            <div class="answer">
                <p> {{ faq.answer }} </p> <!--Extracting answer from faq prop -->
            </div>
        </div>
    </template>
    <style scoped lang="less">
        .faq{
            flex-grow: 1;
            .header{
                display: flex;
                align-items: center;
                justify-content: space-between;
                border: 2px solid antiquewhite;
                padding: 10px;
                border-radius: 6px 6px 0 0;
                cursor: pointer;
                .question{
                    font-weight: 700;
                }
                .icon{
                    width: 30px;
                    height: 30px;
                    transition: transform .5s;
                    img{
                        width: 100%;
                        height: auto;
                    }
                    &.open{
                        transform: rotate(180deg);
                    }
                }
            }
            .answer{
                height: 0;
                overflow-y: scroll;
                line-height: 1.5;
                background-color: antiquewhite;
                transition: height .5s;
                &::-webkit-scrollbar{
                    width: 5px;
                }
                &::-webkit-scrollbar-track{
                    appearance: none;
                    background-color: transparent;
                }
                &::-webkit-scrollbar-thumb{
                    width: 5px;
                    background-color: rgb(232, 210, 182);
                    border-radius: 50px;
                }
                p{
                    padding: 10px;
                }
                &.open{
                    height: 200px;
                }
            }
        }
    </style>
Enter fullscreen mode Exit fullscreen mode

By default answer part of each accordion should be collapsed and we did it in css by giving .answer element height of 0 and setting overflow to scroll so that we can have scroll-bar on .answer if the content is long and it overflows. We also gave some styling to the scroll-bar also.

Adding Functionality

Whenever user clicks on a question or chevron icon it should toggle the answer part of that accordion meaning if answer part is collapsed it should expand and vice-versa. One thing to keep in mind is that click event will occur in Faq.vue component but the data that has to be updated is in Accordion.vue.Because Faq.vue is child component of Accordion.vue we can emit the event from Faq.vue and then listen to that emit event in Accordion.vue. Another thing is that we have to keep track whether the answer part of particular accordion is collapsed or expanded for that we will use isOpen property of faq object in data array.

    <script setup>
        import chevron from '../assets/chevron.svg'
        defineProps(['faq']) // recieving faq object as a prop.
        const emit = defineEmits(['toggleAnswer']) // defining events to emit
        const handleClick = id => emit('toggleAnswer', id) // emitting toggleAnswer event with id attribute.
    </script>
    <template>
        <div class="faq">
            <div class="header" @click="() => handleClick(faq.id)">
                <div class="question">
                    {{ faq.question }} <!--Extracting question from faq prop -->
                </div>
                <div :class="['icon', {open: faq.isOpen}]">
                    <img :src="chevron" alt="chevron-icon"/>
                </div>
            </div>
            <div :class="['answer', {open: faq.isOpen}]">
                <p> {{ faq.answer }} </p> <!--Extracting answer from faq prop -->
            </div>
        </div>
    </template>
    <style scoped lang="less">
        /* Styling goes here */
    </style>
Enter fullscreen mode Exit fullscreen mode

First in Faq.vue we will add click event listener to header element which contains question and chevron icon. When user clicks on header it runs handleClick functions which emits toggleAnswer event to its parent component but to keep track of which accordion has been clicked we also send id of that faq object. Also, in template we are checking if faq.isOpen is true and if it is true then we are adding open class to answer element so that we can expand the answer part of accordion and also we are adding open class to icon element which contains chevron icon because we need to rotate it if faq.isOpen is true.

    <script setup>
        import { ref } from 'vue'
        import Faq from './Faq'

        const faqs = ref(data) // data is above array of faq questions & answers

        const toggleAnswer = id => {
            faqs.value = faqs.value.map(faq => faq.id === id ? {...faq, isOpen: !faq.isOpen} : faq)
        }
    </script>
    <template>
        <main>  <!-- 👈🏻Main container for accordion -->
            <div class="faqs">
                <!-- Looping over faqs array and passing single faq as a prop -->
                <Faq
                    v-for="faq in faqs"
                    :key="faq.id"
                    :faq="faq"
                    @toggle-answer="toggleAnswer"
                />
            </div>
        </main>
    </template>
    <style scoped lang="less">
        /* Styling goes here */
    </style>
Enter fullscreen mode Exit fullscreen mode

In Accordion.vue we are listening to toggle-answer which executes toggleAnswer function which takes id as a parameter sent by Faq.vue. In toggleAnswer function we are updating faqs state by looping over faqs array using map function. Inside map function in each iteration we are checking if id of current item is equal to the id of clicked accordion and if it returns true that means current item is the accordion that was clicked so we are changing its isOpen property to true if it is false and vice-versa.

Another thing that we want in our Accordion is that at one time only one accordion item should have expanded answer part. So, if user clicks on some accordion to expand its answer part then answer parts of other accordion items should get collapsed if they are expanded. For that we first have to set isOpen property of each accordion whose isOpen property is true to false except the one user just clicked by doing this we will make sure that every other accordion have collapsed answer part.

In Accordion.vue

    const toggleAnswer = id => {
        faqs.value = faqs.value.map(faq => faq.isOpen && faq.id !== id ? {...faq, isOpen: false} : faq)
        faqs.value = faqs.value.map(faq => faq.id === id ? {...faq, isOpen: !faq.isOpen} : faq)
    }
Enter fullscreen mode Exit fullscreen mode

I hope you enjoyed this article😊.

💖 💪 🙅 🚩
owais11-art
Owais Khan

Posted on February 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related