Nihar's Dev Corner

How to create a simple Math quiz app

In this article, we will be building a simple quiz web app. It will be made in Vue.js.

It will be a simple flashcard format. A card will have a question and some options. If the option selected is the correct answer, the card will flip over and congratulate us. For this effect, we will be making use of some transition effects. The end result will look like this.

Final result of the quiz app - question and options

Final result of the quiz app - correct answer


First, let's set up our files. Open your terminal, go to the project folder and type the following commands at the terminal. You may select any name for your project. You won't be needing Vuex or Vue-router so don't choose them. The rest of the options are up to you, you can select the ones you want.

#for Vue 3.x
vue create quiz-app

#for Vue 2.x (legacy version)
vue init webpack quiz-app

Our initial files are ready. Open your favorite code editor/IDE and let's get started.

From the boilerplate code, delete the HelloWorld.vue component since we won't be needing it. One of the main components will be the App.vue component. There is a very simple structure for flashcard-based quizzes. There is a question with some options (usually 4) on one side and the answer on the other side. Thus we can put our questions with options to a separate component called Question.vue and put the answer in a separate one called, you guessed it, Answer.vue.

Let's start with App.vue and set up the basic structure of our app. I'll be using Bootstrap v4 in this project. You may use it or any other library that you are familiar with.

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm">
        <h1 class="text-center">
          The Most Awesome Quiz
        </h1>
      </div>
    </div>
    <hr>
    <div class="row">
      <div class="col-sm">
        <transition name="flip" mode="out-in">
          <component :is="mode" @answered="answered($event)" @confirmed="mode = 'Question'"></component>
        </transition>
      </div>
    </div>
  </div>
</template>

We have our heading The Most Awesome Quiz. Then there's a <transition> tag with some attributes. If you're not familiar with the transition tag, it's something that Vue provides us. It allows us to apply transitions to anything by simply wrapping the element with a <transition> tag. Our tag has two attributes - name is the name of the transition and mode="out-in" tells Vue to wait until the previous transition has completely finished before starting a new one.

Inside we have another Vue-provided tag called <component>. This is used for dynamic components.

The basic structure will be like this - we have a dynamic component that will be initially always set to show the Question component. When we select the correct answer among the options, it will switch the component to Answer. This is possible with dynamic components. And we can apply transitions while switching between components thanks to the <transition> tag.

As for the attributes of our <component> tag. The last two are v-ons we are using for custom events. @answered will be a custom event generated by the Question component. It will tell us whether the answer that was selected was the correct one. We can then choose what to do. @confirmed is the one attached to the Answer component. What it does is switch back to the Question component and show a new question.

The first attribute :is is needed for switching between dynamic components. It is a dynamic attribute itself since it will need to change its value.

The rest of the code in the template is just Bootstrap used to add visual goodness to the page so it doesn't look like something from the earlier days of the Internet.

Now comes the core logic for this component. It's quite small (though not as small as the logic for Answer).

import Question from './components/Question.vue';
import Answer from './components/Answer.vue';
export default {
  data() {
    return {
      mode: 'Question'
    }
  },
  components: {
    Question,
    Answer
  },
  methods: {
    answered(isCorrect) {
      if (isCorrect) {
        this.mode = 'Answer';
      } else {
        this.mode = 'Question';
        alert("That's the wrong answer! Try again.")
      }
    }
  }
}

First, we import the two components that we will create. I have put them in a separate /components folder.

We have only one data attribute which will be used to switch between both components dynamically. The only method is used for taking a particular action depending on whether the correct option was picked.

Note that we don't decide whether the correct answer was selected. That is done by the Question component. We simply act on it. If the Question component says that the correct answer was selected, we switch to the Answer component and if it was wrong, we show an alert.

Now that the template and the core logic is done let's quickly finish the transition effects.

.flip-enter-active{
  animation: flip-in 0.5s ease-out forwards;
}
.flip-leave-active{
  animation: flip-out 0.5s ease-out forwards;
}
@keyframes flip-out{
  from{
    transform: rotateY(0deg);
  } 
  to {
    transform: rotateY(90deg);
  }
}
@keyframes flip-in {
  from {
    transform: rotateY(90deg);
  }
  to {
    transform: rotateY(0deg);
  }
}

The classes .flip-enter-active and .flip-leave-active are also provided by Vue when we gave the transition a name (Vue does give us so many nice things). The first class is used when the transition is in the enter stage, meaning it's beginning. The second class is applied when the transition is actively leaving or wrapping up.

You'd be better off seeing the @keyframes in action rather than me explaining it. It has this effect


The Answer component doesn't contain much code since all it does is show a congratulatory message.

<template>
    <div class="alrt alert-success text-center">
        <h1>That's the correct answer!!</h1>
        <hr>
        <button class="btn btn-primary" @click="onNextQuestion">Next Question</button>
    </div>
</template>

The template is quite easy to understand. Just an <h1> and a button that shows the next question.

methods: {
    onNextQuestion() {
        this.$emit('confirmed');
    }
}

The method is invoked by clicking the button and emits a confirmed event to the parent App.vue component. If you remember, when this event is emitted, the App component switches to the Question component.


Now for the final and probably the longest of the 3 components. The Question component has more logic than the previous components since it handles the crucial task of creating new questions and determining whether the correct answer was selected.

<template>
    <div class="container text-center">
        <div class="card">
            <div class="card-body">
                <h3 class="card-title text-center">{{question}}</h3>
                <div class="card-text">
                    <div class="row">
                        <div class="col-sm">
                            <button class="btn btn-primary btn-lg" style="margin: 10px" @click="onAnswer(btnData[0].correct)"> {{btnData[0].answer}} </button>
                        </div>
                        <div class="col-sm">
                            <button class="btn btn-primary btn-lg" style="margin: 10px" @click="onAnswer(btnData[1].correct)"> {{btnData[1].answer}} </button>
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-sm">
                            <button class="btn btn-primary btn-lg" style="margin: 10px" @click="onAnswer(btnData[2].correct)"> {{btnData[2].answer}} </button>
                        </div>
                        <div class="col-sm">
                            <button class="btn btn-primary btn-lg" style="margin: 10px" @click="onAnswer(btnData[3].correct)"> {{btnData[3].answer}} </button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

It seems overwhelming, but it really isn't. A major part of it is Bootstrap. This template displays a card (available in Bootstrap v4) with a simple addition or subtraction question. The numbers are random and we have also randomized the operation. So whether the next question would be an addition or a subtraction is also random.

Next, we have 4 buttons. These will be our options to the question. One of them will be the correct answer (the position of the correct answer also changes randomly by the way). Quite a lot of randomness 😉. But that's what makes this fun!

It would become clear what the interpolated string does after understanding the rest of the code.

const MODE_ADDITION = 1;
const MODE_SUBTRACTION = 2
export default {
    data() {
        return {
            question: 'Oops, an error occured :/',
            btnData: [
                {correct: true, answer: 0},
                {correct: false, answer: 0},
                {correct: false, answer: 0},
                {correct: false, answer: 0}
            ]
        }
    }
}

We have two variables to indicate the mode of operation. The btnData is an array of objects. Each object represents an answer. It has two properties - answer is the answer that the button represents. Each button will have an answer which may or may not be correct. This property will contain that. correct will tell us whether that answer is correct or not.

Even though the correct property of the first object is set to true, it will be changed later.

The question data property will have a string by default. So if our method that generates a question somehow doesn't work, we would know something is wrong.

created() {
    this.generateQuestion();
},

Next, we have this created() lifecycle hook. When this component is created, the generateQuestion() method would be executed. As expected, this method is responsible for generating a new question as well as assigning the correct answer to one of the four buttons.

generateQuestion() {
    const firstNumber = this.generateRandomNumber(1, 100);
    const secondNumber = this.generateRandomNumber(1, 100);
    const modeNumber = this.generateRandomNumber(1, 2);

    let correctAnswer = 0;

    switch (modeNumber) {
        case MODE_ADDITION:
            correctAnswer = firstNumber + secondNumber;
            this.question = `What's ${firstNumber} + ${secondNumber}?`;
            break;
        case MODE_SUBTRACTION:
            correctAnswer = firstNumber - secondNumber;
            this.question = `What's ${firstNumber} - ${secondNumber}?`;
            break;
        default:
            correctAnswer = 0;
            // this.question = 'Oops, an error occurred :/';
    }
    this.btnData[0].answer = this.generateRandomNumber(correctAnswer - 10, correctAnswer + 10, correctAnswer);
    this.btnData[0].correct = false;
    this.btnData[1].answer = this.generateRandomNumber(correctAnswer - 10, correctAnswer + 10, correctAnswer);
    this.btnData[1].correct = false;
    this.btnData[2].answer = this.generateRandomNumber(correctAnswer - 10, correctAnswer + 10, correctAnswer);
    this.btnData[2].correct = false;
    this.btnData[3].answer = this.generateRandomNumber(correctAnswer - 10, correctAnswer + 10, correctAnswer);
    this.btnData[3].correct = false;

    const correctButton = this.generateRandomNumber(0, 3);
    this.btnData[correctButton].correct = true;
    this.btnData[correctButton].answer = correctAnswer;
}

Quite a long function, but don't worry, we'll go through this together.

First of all, we have 3 variables. There is a firstNumber and secondNumber variable which will have a random number between 1 and 100. The third variable modeNumber will represent the mode of operation. There is also a mysterious generateRandomNumber() function. What does that do? It generates a random number for us, but with a slightly different logic. We'll look at it after this function.

Then we have another variable which is probably the most important one - correctAnswer. This will contain the correct answer to our question. Make sure to use let and not const since we need to reassign it.

After declaring our variables we have a switch case. It will check the modeNumber that we randomly chose between 1 and 2. The reason for that is our very first variable declarations where we assigned a number to our operation modes. This will come in handy now.

We can easily change our logic depending on the randomly chosen operation. If the random number was 1, we would add the firstNumber and secondNumber variables and put it as the correct answer. If it was 2, we would subtract them. We then assign the appropriate string to question data property.

Our question is ready and we also have the correct answer to it. Next, we assign it randomly to a button. The next part might seem confusing but it really isn't.

Each button will have it's correct property set to false. A random number will be assigned to the answer property. But we can't just assign a completely random number. For example, if the question was What's 2 + 3? we can't have an option that says 573. That would obviously be the wrong answer. So our options need to be random but still within a range. We make use of the generateRandomNumber() function and pass a minimum number that is 10 less than the correct answer and a maximum number that is 10 more than the correct answer.

Sounds like a good solution doesn't it. But there's a third argument passed to the function, what does that do?

I'm glad you asked. Now we don't want our random option generated to be the actual answer, do we? So the third argument tells the function to generate a random number, within the range we passed, but it shouldn't be the actual answer. Thus, all buttons have the wrong answer.

Now what we do is generate a random index position. Then, we assign the correct answer to the button at this index and set it's correct property to true.

In a nutshell, we have given all the buttons random options and declared they were wrong. Then chose a random button and gave it the correct answer and declared it to be the correct one.

Why did we do it like this though? Couldn't we have chosen a random button, assigned it the answer, and then start assigning wrong answers to the remaining buttons? Sure we could have.

But then, assigning wrong answers to all buttons except the correct one which is randomly selected?! It's a pain. Even though it is possible to somehow do it, I'm lazy ;).

For the remaining functions:

generateRandomNumber(min, max, except) {
    const rndNumber = Math.round(Math.random() * (max - min)) + min;
    if (rndNumber == except) {
        return this.generateRandomNumber(min, max, except);
    }
    return rndNumber;
},
onAnswer(isCorrect) {
    this.$emit('answered', isCorrect);
}

As I explained about the generateRandomNumber() function, it takes 3 arguments. The first two are the range within which a random number is to be generated. The third argument is only used when we are making sure the random number generated is not the correct answer. If it matches the correctAnswer, the function will run recursively until we get a different number.

The onAnswer click handler emits an answered event and passes to the parent (App.vue) component whether the answer was correct or not.


With this, our app is ready. Start a development server to see your code in action.

#for 3.x
npm run serve

#for 2.x
npm run dev

Here is the GitHub repo of the code for reference. I hope you had fun making it. I would love to see you modify and improve upon it. Look forward to hearing from you in the comment section below.


This app is originally from the Vue course on Udemy by Maximilian Schwarzmüller. He teaches lots of amazing things about Vue and you can surely find something of value in that course. So definitely check it out.

I'm currently taking his course and find it valuable. This is not a sponsored post but a personal recommendation.