DEV Community

Cover image for Building a Quiz App with Vuejs & Pinia
Andrew Zachary
Andrew Zachary

Posted on

Building a Quiz App with Vuejs & Pinia

Vuejs is my favorite javascript framework because of its flexibility. In this post, i will share with you my implementation for building a quiz app that features a collection of multiple-choice questions (about animals and birds). Users will have a limited amount of time to answer each question, and failing to do so will result in a deduction of points.

Here is a quick overview of the essential parts of the app:


If you want to follow along with me, it is recommended that you have a good understanding of the basics of Vuejs (how to create a new Vue app).


  • Pinia, is a store library for Vuejs, it allows you to share a state across components/pages.
  • Prime Vue, is a rich set of open source native components for Vue.

Within your Vue app:

npm install pinia primevue@^3 primeicons --save
Enter fullscreen mode Exit fullscreen mode

After installing the necessary packages, we are ready to start creating the app components and store.

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import PrimeVue from 'primevue/config';

import ProgressBar from 'primevue/progressbar';

import App from '../../components/vue/quiz-app/index.vue';

const quizeApp = createApp(App);
const piniaStore = createPinia();

quizeApp.component('ProgressBar', ProgressBar);

Enter fullscreen mode Exit fullscreen mode


  • Timer Component is the heart of the app, this is because it will track the time and update the clock UI when each question becomes available for answering. In case the user runs out of time while answering a question, the component will emit two events:
    • timeout-notifying to update the ui with a timeout message.
    • count-finished to tell Question Component (its parent component) that time's up.
<script setup>
    import { onMounted, onUnmounted, ref } from 'vue';
    import { useQuizStore } from '../../../js/vue/stores/quiz.js';

    const {duration} = defineProps(['duration']);
    const emit = defineEmits(['count-finished', 'timeout-notifying']);

    const quizStore = useQuizStore();

    const timerInterval = ref(null);
    const questionDurationProgress = ref(0);
    const currentSecound = ref(0);
    const timeOut = ref(false);

    const startTimer = () => {
        timerInterval.value = setInterval((x) => {
            currentSecound.value += 1;
            questionDurationProgress.value = (currentSecound.value / (duration-1)) * 100;
            if(currentSecound.value === duration) {
                timeOut.value = true;
        }, 1000);

    const finishing = () => {
        setTimeout(() => {
        }, 2000);

    onMounted(() => {
<style lang="scss"></style>
    <div class="px-8">
        <h1 class="font-mont font-light text-8xl text-white flex justify-center">
            <div class="inline-block p-20 leading-none rounded-full bg-black relative">
                <span class="absolute top-1/2 left-1/2 translate-y-[-50%] translate-x-[-50%]">{{ currentSecound }}</span>
        <div class="mt-8">
            <h1 class="font-mont font-bold text-4xl text-center capitalize animate__animated animate__tada" v-if="timeOut">time out !!!</h1>
            <ProgressBar v-else :value="questionDurationProgress" :showValue="false" />
Enter fullscreen mode Exit fullscreen mode
  • Question Component will render the question content including the answers and the timer. Each answer is clickable, picking up an answer will emit answer-picked event. The component also is listening to the timer's count-finished emitted event, and depending on it to emit its own no-answer-picked event. Emitting either answer-picked or no-answer-picked events will call:
    • updateProgress store action that will record the user progress while answering the questions.
    • paginate store action that will load the next question.
<script setup>
    import { ref } from 'vue';
    import Timer from './timer.vue';

    const { content } = defineProps(['content']);
    const emit = defineEmits(['answer-picked', 'no-answer-picked']);
    const timeout = ref(false);

    const noAnswerPicked = () => {
        emit('no-answer-picked', content, null);

    const answerPicked = (answerId) => {
        emit('answer-picked', content, answerId);

    const notifyAboutTimeout = () => {
        timeout.value = true
    <div class="animation-box">
        <Timer :duration="10" @timeoutNotifying="notifyAboutTimeout" @countFinished="noAnswerPicked" />
        <div v-if="!timeout" class="question-content capitalize p-4 mt-8 overflow-hidden">
            <div class="content-box w-full max-w-sm mx-auto">
                <h1 class="text-4xl font-mont font-bold">{{ content.body }}</h1>
                <ul class="mt-8">
                    <li class="text-3xl font-ssp mt-4" v-for="answer of content.answers" :key="">
                        <span class="p-2 cursor-pointer hover:border-2 hover:border-black" @click="answerPicked(">{{ answer.body }}</span>
<style lang="scss"></style>
Enter fullscreen mode Exit fullscreen mode
  • App Component will wrap all other UI components and connect them to the store through methods (for example) loadNextQuestion method will call two store actions updateProgress and paginate, then passing the payload to them.
<script setup>
    import { onMounted } from 'vue';

    import Question from './question.vue';

    import { useQuizStore } from '../../../js/vue/stores/quiz';

    const quizStore = useQuizStore();

    onMounted(() => {

    const loadNextQuestion = (question, answerId) => {
        quizStore.updateProgress(question, answerId);
<style lang="scss"></style>
    <transition name="fade" mode="out-in">
        <div v-if="quizStore.quizFinished" class="p-2 flex flex-col items-center">
            <h1 class="text-6xl font-mont font-bold capitalize">
            <h2 class="text-3xl font-ssp font-bold capitalize mt-2">
                <span class="px-2">score: {{ quizStore.score }} / 5</span>
            <div class="mt-4 px-2">
                    <li class="mt-8" v-for="row in quizStore.result">
                        <h1 class="text-4xl font-mont font-bold">{{ row.body }}</h1>
                        <div class="text-2xl font-ssp capitalize mt-4">
                            <div class="feedback flex items-center">
                                <span>{{ row.userAnswer.body }}</span>
                                <span class="px-3.5 py-3 m-2 bg-black text-white rounded-full flex justify-center items-center max-w-[3rem]"><i :class="['pi', row.answerisRight?'pi-check':'pi-times']"></i></span>
                            <div v-if="!row.answerisRight">
                                <span class="font-bold">right answer is </span>
                                <span class="font-regular">{{ row.rightAnswer.body }}</span> 
            <div class="mt-4">
                <button class="text-4xl font-ssp font-bold bg-black text-white capitalize px-4 py-2" @click="quizStore.restartQuiz">restart quiz</button>
        <div v-else id="question-box" class="relative overflow-hidden">
            <template v-for="question of quizStore.quiz.items" :key="">
                <transition name="fade" mode="out-in">
                        v-if=" === quizStore.currentQuestionId" 
Enter fullscreen mode Exit fullscreen mode

Define a Quiz Store

Consider it a small app that will hold all business logic. In addition to the actions we already discussed, you also can declare getters within the store as a computed properties. Within this app (for example) the quiz computed property was used to generate a unique random collection of questions.

When the quiz round has finished the store needs to be reset this can be done through restartQuiz action. You can also change the value of version prop and this will tell quiz getter (computed) prop to regenerate a new collection of questions, because version prop is one of the quiz computed prop dependencies.

import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

export const useQuizStore = defineStore('quiz', () => {
    const questions = [
            id: 1,
            body: 'which is the tallest animal in the world?',
            answers: [
                {id:1, body: 'giraffe'},
                {id:2, body: 'lion'},
                {id:3, body: 'cheetah'},
                {id:4, body: 'bat'}
            rightAnswerId: 1

    const currentQuestionIndex = ref(0);
    const currentQuestionId = ref(null);
    const quizFinished = ref(false);
    const version = ref(1);
    const result = ref([]);
    const score = ref(0);

    const quiz = computed(() => {
        const max = questions.length - 1;
        const min = 0;
        const uniqueNumbers = new Set();
        while (uniqueNumbers.size < 5) {
            const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min;

        const questionsCollection = [...uniqueNumbers].map(index => questions[index]);

        return {
            version: version.value,
            items: questionsCollection

    const updateProgress = (question, answerId) => {

        const answerisRight = question.rightAnswerId === answerId;

        if(answerisRight) score.value += 1;

        result.value = [...result.value, {
            userAnswer: question.answers.filter((choice) => answerId ===[0] ?? {body:'no answer'},
            rightAnswer: question.answers.filter((choice) => question.rightAnswerId ===[0]

    const paginate = () => {
        if(currentQuestionIndex.value === quiz.value.items.length) return quizFinished.value = true;
        currentQuestionId.value = quiz.value.items[currentQuestionIndex.value].id;
        currentQuestionIndex.value += 1;

    const restartQuiz = () => {
        currentQuestionIndex.value = 0;
        currentQuestionId.value = null;
        quizFinished.value = false;
        result.value = [];
        score.value = 0;

        version.value += 1;


    return { 
Enter fullscreen mode Exit fullscreen mode

Full code

Working example


Vuejs features rich ecosystem, Pinia is a very powerfull state management library that helps you manage and store reactive data and state across your components in your Vuejs applications. The components necessary for creating the App are wrapping the UI, and pinia quiz store is responsible for isolating the App's working logic.

Your feedback is much appreciated, thank you for reading.

Top comments (2)

nickap profile image

Nice! I was thinking of a similar app targeted to developers!
You know just helping devs keep contact with a technology.

Nice article!

andrewzach profile image
Andrew Zachary

It's great you like it nickap