Create a Dynamic Quiz Card Component with React, TypeScript, and Tailwind CSS

In modern web applications, creating interactive and user-friendly components is essential. One such component is the quiz card.

In this article, I'll walk you through building a dynamic quiz card component using React, TypeScript, and Tailwind CSS. This code will cover the main features, including a responsive design, interactive elements, and a countdown timer.

Key Features of the Quiz Card

Our quiz card component includes 5 important features:

  1. The "Next" button is disabled until an answer is selected.
  2. Displaying the current question number and the total number of questions.
  3. A countdown timer to enhance the quiz experience.
  4. Selected answers are highlighted for better user interaction.
  5. Wrote a util funciton addLeadingZero to add a leading zero if number is less than 10

This quiz card is built with Typescript and Tailwind CSS, using the best React practices.

Important Points

Here are 4 key implementation details to make our quiz card component modular and efficient:

  1. Reusable Quiz Option Component: Extracted the quiz option logic into a reusable component for better maintainability.
  2. Separation of Data: Used a separate file to manage quiz questions data.
  3. Icon Library: Integrated a third-party icon library for the timer icon to save development time. You can use icon of your choice.
  4. Consistent Theming: Applied consistent background and text colors in body to maintain a theme.
body { @apply bg-[#0f172a] text-white; font-family: 'Anek Malayalam', sans-serif; }

Quiz Card UI

This is how Quiz Card component UI looks like: Tailwind Quiz component UI

Tailwind Quiz Card Component Code

import { Timer } from 'lucide-react' import { useEffect, useState } from 'react' import { quiz } from '../appData/QuizQuestions' import QuizOption from './QuizOption' const Quiz = () => { const [activeQuestion, setActiveQuestion] = useState<number>(0) const [selectedAnswerIndex, setSelectedAnswerIndex] = useState<number | null>( null, ) const [timer, setTimer] = useState(60) const { questions } = quiz const { question, choices } = questions[activeQuestion] const onClickNext = () => { setSelectedAnswerIndex(null) if (activeQuestion !== questions.length - 1) { setActiveQuestion((prev) => prev + 1) } else { setActiveQuestion(0) } } const onAnswerSelected = (answer: string, index: number) => { setSelectedAnswerIndex(index) } useEffect(() => { if (timer > 0) { const countdown = setInterval(() => setTimer(timer - 1), 1000) return () => clearInterval(countdown) } }, [timer]) const addLeadingZero = (number: number) => number > 9 ? number : `0${number}` return ( <div className="mx-auto mt-[100px] max-w-3xl rounded-md border border-[#444444] bg-[#1e293b] px-[60px] py-[30px]"> <div className="flex items-center justify-between"> <div> <span className="text-4xl font-medium text-[#38bdf8]"> {addLeadingZero(activeQuestion + 1)} </span> <span className="text-[22px] font-medium text-[#817a8e]"> /{addLeadingZero(questions.length)} </span> </div> <div className="flex w-[100px] items-center gap-2"> <Timer color="#38bdf8" width={28} height={28} /> <span className="mt-1 block text-2xl font-medium text-[#38bdf8]"> 00:{addLeadingZero(timer)} </span> </div> </div> <h3 className="my-4 text-2xl font-medium">{question}</h3> <form> {choices.map((answer, index) => ( <QuizOption key={answer} index={index} answer={answer} selectedAnswerIndex={selectedAnswerIndex} onAnswerSelected={onAnswerSelected} /> ))} </form> <div className="flex justify-end"> <button onClick={onClickNext} disabled={selectedAnswerIndex === null} className="mt-12 min-w-[150px] transform cursor-pointer rounded-lg border border-[#38bdf8] bg-[#38bdf8] px-5 py-1.5 text-lg font-semibold text-white outline-none transition duration-300 ease-in-out hover:scale-105 hover:bg-[#1d4ed8] active:scale-95 active:bg-[#1e40af] disabled:cursor-not-allowed disabled:border-gray-500 disabled:bg-gray-800 disabled:text-gray-500 disabled:hover:scale-100" > {activeQuestion === questions.length - 1 ? 'Finish' : 'Next'} </button> </div> </div> ) } export default Quiz

Quiz Option Component Code

Here’s the code for the QuizOption component, which handles individual quiz choices.

interface QuizOptionProps { index: number answer: string selectedAnswerIndex: number | null onAnswerSelected: (answer: string, index: number) => void } const QuizOption: React.FC<QuizOptionProps> = ({ index, answer, selectedAnswerIndex, onAnswerSelected, }) => { return ( <div className="relative mt-4 cursor-pointer"> <input type="radio" id={`choice-${index}`} name="quiz" value={answer} checked={selectedAnswerIndex === index} onChange={() => onAnswerSelected(answer, index)} className="hidden" /> <label htmlFor={`choice-${index}`} className={`block cursor-pointer rounded-lg border border-[#333] bg-[#0f172a] px-4 py-3 text-lg text-white transition-colors duration-300 ease-in-out ${ selectedAnswerIndex === index ? 'border-[#2f459c] bg-[#2f459c]' : '' }`} > {answer} </label> </div> ) } export default QuizOption

Quiz Timer Logic

The quiz component includes a countdown timer to keep track of time.

Here’s how to set it up:

  1. Declare state for timer and set initails value to 60 (seconds)
  2. In the useEffect hook, we wrote the logic for counting down the time.
const [timer, setTimer] = useState(60) useEffect(() => { if (timer > 0) { const countdown = setInterval(() => setTimer(timer - 1), 1000) return () => clearInterval(countdown) } }, [timer])

Quiz Question Data Structure

Here’s the structure of the quiz data used in our application.

interface Question { question: string choices: string[] type: string correctAnswer: string } interface QuizData { topic: string level: string totalQuestions: number perQuestionScore: number totalTime: number // in seconds questions: Question[] } export const quiz: QuizData = { topic: 'Javascript', level: 'Beginner', totalQuestions: 4, perQuestionScore: 5, totalTime: 60, questions: [ { question: 'Which function is used to serialize an object into a JSON string in Javascript?', choices: ['stringify()', 'parse()', 'convert()', 'None of the above'], type: 'MCQs', correctAnswer: 'stringify()', }, { question: 'Which of the following keywords is used to define a variable in Javascript?', choices: ['var', 'let', 'var and let', 'None of the above'], type: 'MCQs', correctAnswer: 'var and let', }, { question: 'Which of the following methods can be used to display data in some form using Javascript?', choices: [ 'document.write()', 'console.log()', 'window.alert', 'All of the above', ], type: 'MCQs', correctAnswer: 'All of the above', }, { question: 'How can a datatype be declared to be a constant type?', choices: ['const', 'var', 'let', 'constant'], type: 'MCQs', correctAnswer: 'const', }, ], }

Feel free to use this Quiz Card component in your projects.


Flexy UI Newsletter

Build better and faster UIs.Get the latest components and hooks directly in your inbox. No spam!