Simple Quiz Application with Progress Bar Using HTML, CSS, and Vue.js
By Bytewebster - August 3, 2023
Welcome to Bytewebster JavaScript projects on creating a Simple Quiz Application with Progress Bar Using HTML, CSS, and Vue.js.
Before this, we had a similar quiz app on our website, which was built using plain JavaScript.
However, in this version, we have used Vue.js to enhance both the user interface and experience.
Working
This Vue.js quiz app will have many questions, and each question will have four options to choose from. You can customize the number of options if you want. In the app, you'll find two buttons: "Back" and "Skip." You can use these buttons to go back to the previous question or skip the current one
and come back to it later to change your answer. also there will be a progress bar at the top of the app that shows you how much of the quiz you've completed.
Once you finish the quiz, your score will be automatically calculated, showing you the number of correct answers out of the total questions attempted.
HTML Structure
If you're not familiar with Vue.js and find the quiz app's structure a bit complicated, don't worry. We'll do our best to explain it in an easy and understandable way so let's get started.
The HTML code of this quiz application is wrapped inside a section element with a class of container, basically it is a container for the quiz app. And the quiz app is defined by a Vue object with the ID "app."
There is a transition element which is used to add animation effects when questions are displayed and hidden. also there are two main attributes the first one is enter-active-class and the second one is leave-active-class. They specify the CSS classes to be applied during the animation.
<section class="container">
<!--questionBox-->
<div class="questionBox" id="app">
<!-- transition -->
<transition :duration="{ enter: 500, leave: 300 }" enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" mode="out-in">
<!--qusetionContainer-->
<div class="questionContainer" v-if="questionIndex<quiz.questions.length" v-bind:key="questionIndex">
<header>
<h1 class="title is-6" style="font-family: arial;">Vue.js Quiz</h1>
<!--progress-->
<div class="progressContainer">
<progress class="progress is-info is-small" :value="(questionIndex/quiz.questions.length)*100" max="100">{{(questionIndex/quiz.questions.length)*100}}%</progress>
<p>{{(questionIndex/quiz.questions.length)*100}}% complete</p>
</div>
<!--/progress-->
</header>
<!-- questionTitle -->
<h2 class="titleContainer title">Q. {{ quiz.questions[questionIndex].text }}</h2>
<!-- quizOptions -->
<div class="optionContainer">
<div class="option" v-for="(response, index) in quiz.questions[questionIndex].responses" @click="selectOption(index)" :class="{ 'is-selected': userResponses[questionIndex] == index}" :key="index">
{{ index | charIndex }}. {{ response.text }}
</div>
</div>
<!--quizFooter: navigation and progress-->
<footer class="questionFooter">
<!--pagination-->
<nav class="pagination" role="navigation" aria-label="pagination">
<!-- back button -->
<a class="button" v-on:click="prev();" :disabled="questionIndex < 1"> <i class="bx bx-arrow-back"></i> Back </a>
<!-- next button -->
<a class="button" :class="(userResponses[questionIndex]==null)?'':'is-active'" v-on:click="next();" :disabled="questionIndex>=quiz.questions.length">
<i class="bx bx-skip-next"></i> {{ (userResponses[questionIndex]==null)?'Skip':'Next' }}
</a>
</nav>
<!--/pagination-->
</footer>
<!--/quizFooter-->
</div>
<!--/questionContainer-->
<!--quizCompletedResult-->
<div v-if="questionIndex >= quiz.questions.length" v-bind:key="questionIndex" class="quizCompleted has-text-centered">
<!-- quizCompletedIcon: Achievement Icon -->
<span class="icon">
<i class="fa" :class="score()>3?'fa-check-circle-o is-active':'fa-times-circle'"></i>
</span>
<!--resultTitleBlock-->
<h2 class="title">
You did {{ (score()>7?'an amazing':(score()<4?'a poor':'a good')) }} job!
</h2>
<p class="subtitle">
Total score: {{ score() }} / {{ quiz.questions.length }}
</p>
<br />
<a class="button" @click="restart()">restart <i class="fa fa-refresh"></i></a>
<!--/resultTitleBlock-->
</div>
<!--/quizCompetedResult-->
</transition>
</div>
<!--/questionBox-->
</section>
Next, The questionContainer element is used to display the questions and options to the user. It uses v-if directive to show only if the current questionIndex is less than the total number of questions in the quiz.
The header section displays the title of the quiz and a progress bar showing the completion percentage of the quiz. and the footer section contains two navigation buttons first one is Back button which is disabled when the user is on the first question, and a Next button, which is disabled when the user is on the last question or if they haven't selected an option for the current question.
Styling With CSS
We are now moving towards the CSS part to give this quiz app a better look. So far, only the structure of this quiz app has been prepared. There is still a lot of work left, but for now, let's understand its designing.
The CSS code of this vue.js quiz application starts by defining font styles for .title and .subtitle. These styles are applied to the title and subtitle texts of the quiz. Next, after this code we have a class called .animated this class is used to create smooth transition effects.
After that the .container class sets the margins of the quiz container to create some space. also there is the styling for questionBox class, It sets the width and height of the box and gives it a nice background color.
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,400i,700");
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700");
body {
font-family: "Open Sans", sans-serif;
font-size: 14px;
height: 100vh;
background: #CFD8DC;
cursor: default !important;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
user-drag: none;
display: flex;
align-items: center;
justify-content: center;
background-image: linear-gradient( 135deg, #FAB2FF 10%, #1904E5 100%);
}
.button {
transition: 0.3s;
}
.title,
.subtitle {
font-family: Montserrat, sans-serif;
font-weight: normal;
}
.animated {
transition-duration: 0.15s;
}
.container {
margin: 0 0.5rem;
}
.questionBox {
max-width: 30rem;
width: 35rem;
min-height: 30rem;
background: #FAFAFA;
position: relative;
display: flex;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
}
.questionBox header {
background-image: radial-gradient( circle 369px at -2.9% 12.9%, rgba(247,234,163,1) 0%, rgba(236,180,238,0.56) 46.4%, rgba(163,203,247,1) 100.7% );
padding: 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.questionBox header h1 {
font-weight: bold;
margin-bottom: 1rem !important;
}
.questionBox header .progressContainer {
width: 60%;
margin: 0 auto;
}
.questionBox header .progressContainer > progress {
margin: 0;
border-radius: 5rem;
overflow: hidden;
border: none;
color: #3D5AFE;
}
.questionBox header .progressContainer > progress::-moz-progress-bar {
background: #3D5AFE;
}
.questionBox header .progressContainer > progress::-webkit-progress-value {
background: #3D5AFE;
}
.questionBox header .progressContainer > p {
margin: 0;
margin-top: 0.5rem;
}
.questionBox .titleContainer {
text-align: center;
margin: 0 auto;
padding: 1.5rem;
}
.questionBox .quizForm {
display: block;
white-space: normal;
height: 100%;
width: 100%;
}
.questionBox .quizForm .quizFormContainer {
height: 100%;
margin: 15px 18px;
}
.questionBox .quizForm .quizFormContainer .field-label {
text-align: left;
margin-bottom: 0.5rem;
}
.questionBox .quizCompleted {
width: 100%;
padding: 1rem;
text-align: center;
}
.questionBox .quizCompleted > .icon {
color: #FF5252;
font-size: 5rem;
}
.questionBox .quizCompleted > .icon .is-active {
color: #00E676;
}
.questionBox .questionContainer {
white-space: normal;
height: 100%;
width: 100%;
}
.questionBox .questionContainer .optionContainer {
margin-top: 12px;
flex-grow: 1;
}
.questionBox .questionContainer .optionContainer .option {
border-radius: 290486px;
padding: 9px 18px;
margin: 0 18px;
margin-bottom: 12px;
transition: 0.3s;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.85);
border: transparent 1px solid;
}
.questionBox .questionContainer .optionContainer .option.is-selected {
border-color: rgba(0, 0, 0, 0.25);
background-color: white;
}
.questionBox .questionContainer .optionContainer .option:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.questionBox .questionContainer .optionContainer .option:active {
transform: scaleX(0.9);
}
.questionBox .questionContainer .questionFooter {
background-image: linear-gradient(-225deg, #FFFEFF 0%, #D7FFFE 100%);
border-top: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
align-self: flex-end;
}
.questionBox .questionContainer .questionFooter .pagination {
margin: 15px 25px;
}
.pagination {
display: flex;
justify-content: space-between;
}
.button {
padding: 0.5rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 5rem;
margin: 0 0.25rem;
transition: 0.3s;
}
.button:hover {
cursor: pointer;
background: #ECEFF1;
border-color: rgba(0, 0, 0, 0.25);
}
.button.is-active {
background: #3D5AFE;
color: white;
border-color: transparent;
}
.button.is-active:hover {
background: #0a2ffe;
}
@media screen and (min-width: 769px) {
.questionBox {
align-items: center;
justify-content: center;
}
.questionBox .questionContainer {
display: flex;
flex-direction: column;
}
}
@media screen and (max-width: 768px) {
.sidebar {
height: auto !important;
border-radius: 6px 6px 0px 0px;
}
}
Next, the progress bar has a blue color and a smooth appearance with rounded corners. the options class styles each quiz option it gives them a rounded border, some padding, and a smooth transition effect of 0.3 seconds when hovered over.
in the navigation buttons area the pagination class styles the navigation buttons in the footer It adds some margin and centers the buttons. lastly in the CSS code includes some media queries to adjust the layout for different screen sizes.
If you want to make any changes in this, you can freely modify it without hesitation. You can design it using Tailwind CSS or Bootstrap, whichever you prefer.
JavaScript Explanation
Now comes the final and most important part, which is JavaScript. Without it, it is not possible to create this vue quiz app. Let's now understand this part. first it defines a quiz object that contains information about the quiz.
It includes a user property to store the name of the user taking the quiz and a questions array that holds individual question objects. each question object has two properties a string representing the question itself an array of response options, where each option is an object and an optional correct property indicating if the response is correct.
The code initializes the userResponses array, It is used to track the user's responses to each question. after that a new Vue instance is created and bound to the element with the ID app. The data property of the Vue instance contains the data used in the app.
var quiz = {
user: "Dave",
questions: [
{
text: "Texts that are enclosed on a <title> tag are all displayed in which part of the browser?",
responses: [
{ text: "Tab" },
{ text: "Title Bar", correct: true },
{ text: "Menu Bar" },
{ text: "Tool Bar" }] },
{
text: "Tags that can stand alone are called…",
responses: [
{ text: "Empty Tag", correct: true },
{ text: "Markup Tag" },
{ text: "Container Tag" },
{ text: "Standalone Tag" }] },
{
text: "Which tag is used to create body text in HTML?",
responses: [
{ text: "HEAD" },
{ text: "BODY", correct: true },
{ text: "TITLE" },
{ text: "TEXT" }] },
{
text: "Outlook Express is _________",
responses: [
{ text: "E-Mail Client", correct: true },
{ text: "Browser" },
{
text: "Search Engine" },
{ text: "None of the above" }] },
{
text: "What is a search engine?",
responses: [
{ text: "A hardware component " },
{
text: "A machinery engine that search data" },
{ text: "A web site that searches anything", correct: true },
{ text: "A program that searches engines" }] },
{
text:
"What does the .com domain represents?",
responses: [
{ text: "Network" },
{ text: "Education" },
{ text: "Commercial", correct: true },
{ text: "None of the above" }] },
{
text: "In Satellite based communication, VSAT stands for? ",
responses: [
{ text: " Very Small Aperture Terminal", correct: true },
{ text: "Varying Size Aperture Terminal " },
{
text: "Very Small Analog Terminal" },
{ text: "None of the above" }] },
{
text: "What is the full form of TCP/IP? ",
responses: [
{ text: "Telephone call protocol / international protocol" },
{ text: "Transmission control protocol / internet protocol", correct: true },
{ text: "Transport control protocol / internet protocol " },
{ text: "None of the above" }] },
{
text:
"What is the full form of HTML?",
responses: [
{
text: "Hyper text marking language" },
{ text: "Hyphenation text markup language " },
{ text: "Hyper text markup language", correct: true },
{ text: "Hyphenation test marking language" }] },
{
text: "\"Yahoo\", \"Infoseek\" and \"Lycos\" are _________?",
responses: [
{ text: "Browsers " },
{ text: "Search Engines", correct: true },
{ text: "News Group" },
{ text: "None of the above" }] }] },
userResponseSkelaton = Array(quiz.questions.length).fill(null);
var app = new Vue({
el: "#app",
data: {
quiz: quiz,
questionIndex: 0,
userResponses: userResponseSkelaton,
isActive: false },
filters: {
charIndex: function (i) {
return String.fromCharCode(97 + i);
} },
methods: {
restart: function () {
this.questionIndex = 0;
this.userResponses = Array(this.quiz.questions.length).fill(null);
},
selectOption: function (index) {
Vue.set(this.userResponses, this.questionIndex, index);
//console.log(this.userResponses);
},
next: function () {
if (this.questionIndex < this.quiz.questions.length)
this.questionIndex++;
},
prev: function () {
if (this.quiz.questions.length > 0) this.questionIndex--;
},
// Return "true" count in userResponses
score: function () {
var score = 0;
for (let i = 0; i < this.userResponses.length; i++) {
if (
typeof this.quiz.questions[i].responses[
this.userResponses[i]] !==
"undefined" &&
this.quiz.questions[i].responses[this.userResponses[i]].correct)
{
score = score + 1;
}
}
return score;
//return this.userResponses.filter(function(val) { return val }).length;
} } });
Also, The Vue instance defines several methods that provide functionality to this quiz app. like the restart method this method resets the quiz by setting the questionIndex back to 0 and clearing the userResponses array, effectively restarting the quiz. and the selectOption method this method is called when the user selects an option for a question it updates the userResponses array. also the next and prev methods these methods increments and decrements the questionIndex.
Mainly, this JavaScript code sets up a Vue.js app to handle a quiz. It defines the quiz data, tracks user responses, and provides methods to navigate through the questions.
We have made every effort from our end to help you understand how this vue js quiz application works, but despite that, if you have any remaining queries, you can absolutely contact us.
We are grateful for your time and attention, and we trust that you have found the project to be interesting.
Video of the Project
Take This Short Survey!
Download Source Code Files
From here You can download the source code file of this Vue.js Quiz Application with Progress Bar.
If you are just starting in web development, these snippets will be useful. We would appreciate it if you would share our blog posts with other like-minded people.
ByteWebster Play and Win Offer.
PLAY A SIMPLE GAME AND WIN PREMIUM WEB DESIGNS WORTH UPTO $100 FOR FREE.
PLAY FOR FREE