Build a JLPT N3 Grammar Quiz App

Build a browser-based quiz app that drills JLPT N3 grammar patterns.

Introduction

30 Second Summary

Studying for a language test by reading grammar explanations only gets you so far. Real retention comes from active recall, where you test yourself and get immediate feedback on what you know and what you don't.

In this project, you will build a browser-based JLPT N3 grammar quiz app that drills you on 12 real test-format questions, gives instant feedback with explanations, and tracks your score.

What You'll Build

A single-page quiz app using HTML, CSS, and JavaScript that presents JLPT N3 grammar questions in the same fill-in-the-blank format as the real test.

By the end of this project, you'll have:

  • A styled quiz interface with a question counter, score display, and colour-coded answer feedback.
  • 12 multiple-choice grammar questions covering high-priority N3 patterns like ~はずだ, ~ことにする, and ~ために.
  • Foundational JavaScript skills including variables, functions, DOM manipulation, and event listeners.
  • Secret Mission: a Review Mistakes mode that collects your wrong answers and lets you retry just those questions.

Are there any prerequisites?

You should have basic HTML knowledge (enough to recognize tags like <div> and <p>). For the Japanese content, familiarity with Minna no Nihongo through lesson 28 (early N4 level) is helpful so the question sentences feel readable.

Before We Start

Before We Start...

Set Up the Project

Before writing any quiz logic, you need a clean project folder with the right file structure. Every web app starts with at least three files: one for structure (HTML), one for styling (CSS), and one for behavior (JavaScript).

In this step, you'll create the project folder, set up those three files, and write a minimal HTML skeleton that connects them all together. Once the page loads in your browser without errors, you're ready to build.

In this step, get ready to:
  • Create the n3-quiz project folder on your Desktop.
  • Create the three project files: index.html, style.css, and script.js.
  • Write the HTML skeleton and confirm the page loads in your browser.
Create the project folder

Your quiz needs its own folder to keep the HTML, CSS, and JavaScript files organized together. You'll create this folder on your Desktop so it's easy to find.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (the backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir n3-quiz
cd n3-quiz

Now open this folder as your VS Code workspace.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the n3-quiz folder, and click Select Folder (Windows) or Open (macOS).

VS Code's Explorer sidebar now shows N3-QUIZ as the workspace name. The folder is empty for now.

Don't see N3-QUIZ in the sidebar?

Make sure you selected the n3-quiz folder itself, not the Desktop. The sidebar title should match the folder name.

If the mkdir command gave an error, check that you ran cd ~/Desktop first. The tilde (~) points to your user folder.

Help me open my n3-quiz folder in VS Code

Create the project files

Every web page needs an HTML file as the entry point. The CSS and JavaScript files are separate so your code stays organized as the project grows.

  • Open the integrated terminal by pressing Ctrl+` (it should already show n3-quiz as the working directory).
  • Create all three project files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item index.html, style.css, script.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js in the output. They also appear in VS Code's Explorer sidebar on the left.

Don't see all three files?

On Windows, make sure you included the commas between filenames in the New-Item command. Without commas, PowerShell only creates the first file.

On macOS, check that the filenames are separated by spaces (no commas).

Help me create my project files

Write the HTML skeleton

The HTML file is the entry point for your quiz app. It needs to tell the browser which CSS and JavaScript files to load. Without these links, your styling and quiz logic would never run.

  • Click on index.html in the Explorer sidebar to open it.
  • Add the HTML skeleton by pasting this code:
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JLPT N3 Grammar Quiz</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <script src="script.js"></script>
</body>
</html>

What does this code do?

  • lang="ja" tells the browser this page uses Japanese, which helps with text rendering and accessibility.
  • The viewport meta tag ensures the quiz scales properly on mobile screens.
  • The <link> tag connects your CSS file for styling.
  • The <script> tag at the bottom of <body> loads your JavaScript after the page structure is ready.
  • Save index.html by pressing Ctrl+S (Windows) or Cmd+S (macOS).
  • Open the file in your browser. In the Explorer sidebar, right-click index.html and select Reveal in File Explorer (Windows) or Reveal in Finder (macOS). Then double-click index.html to open it in your default browser.
  • Press F12 to open DevTools and check the Console tab. You should see a blank white page with no red error messages in the console.

Seeing errors in the console?

If you see an error about style.css or script.js not being found, make sure all three files are in the same folder. Run ls in your terminal to confirm.

Check for typos in the href and src values. They must match your filenames exactly, including lowercase letters.

Help me fix my HTML page errors

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then you're ready to move on.

ⓧ I'd like to double check the full code

Here is the complete index.html at this point. Compare it against yours:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JLPT N3 Grammar Quiz</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <script src="script.js"></script>
</body>
</html>

The style.css and script.js files should still be empty at this point.

Your project skeleton is ready. All three files are connected and the browser loads them without errors. Next up, you'll build the quiz interface with a question area, answer buttons, and a results screen.

Build the Quiz Interface

Your project files are ready and loading in the browser. Now it's time to build the visual interface that your quiz will use to display questions and collect answers.

In this step, you'll create the HTML elements that make up the quiz screen (question area, answer buttons, score counter) and style them with CSS for a clean, dark-themed look with color-coded feedback.

In this step, get ready to:
  • Add the quiz screen HTML with a question area and four answer buttons.
  • Add a hidden results screen for the end of the quiz.
  • Style the interface with a dark theme and color-coded feedback.
Add the quiz screen HTML structure

The quiz screen is the main view your learner interacts with. It needs a question counter at the top, a space for the question text, four clickable answer buttons, an area for the explanation after answering, and a "Next" button to advance.

  • In VS Code, open index.html from the Explorer sidebar.
  • Find the <script src="script.js"></script> line inside the <body> tag.
  • Add the following HTML directly above that script line:
    <div class="quiz-container">
        <div class="quiz-header">
            <span id="question-counter">Question 1/12</span>
            <span id="score-display">Score: 0</span>
        </div>

        <div id="quiz-screen">
            <p id="question-text" class="question-text"></p>
            <div id="answers-container" class="answers-container">
                <button class="answer-btn" data-index="0"></button>
                <button class="answer-btn" data-index="1"></button>
                <button class="answer-btn" data-index="2"></button>
                <button class="answer-btn" data-index="3"></button>
            </div>
            <p id="explanation" class="explanation"></p>
            <button id="next-btn" class="next-btn">Next</button>
        </div>
    </div>

What does this HTML do?

  • .quiz-container wraps everything into one centered card on the page.
  • .quiz-header holds the question counter and score side by side at the top.
  • #quiz-screen is the main view. It contains the question text, four answer buttons (each with a data-index attribute so JavaScript can identify which button was clicked), an explanation area, and a "Next" button.
  • Save index.html.
  • Refresh your browser. You should see the text "Question 1/12", "Score: 0", and a "Next" button. The buttons won't be styled yet.

Don't see anything in the browser?

  • Make sure you pasted the HTML above the <script> tag, not below it or outside the <body>.
  • Check that you saved the file. Unsaved changes won't appear in the browser.
  • Open DevTools with F12 and check the Console tab for any red error messages.

Why isn't my HTML showing in the browser?

Add the results screen HTML

When the quiz finishes, the app needs a separate screen to show the final score and let the learner try again. This screen stays hidden until all questions are answered.

  • In index.html, find the closing </div> of the quiz-container (the last </div> before the script tag).
  • Add the following results screen HTML just above that closing </div>, right after the quiz-screen's closing </div>:
        <div id="results-screen" class="results-screen hidden">
            <h2>Quiz Complete!</h2>
            <p id="final-score" class="final-score"></p>
            <p id="result-message" class="result-message"></p>
            <button id="try-again-btn" class="try-again-btn">Try Again</button>
        </div>

What does the hidden class do?

The hidden class will set display: none on the results screen so it's invisible when the quiz starts. Later, your JavaScript will remove this class to reveal the results when the quiz ends.

  • Save index.html.
  • Refresh your browser. You should now also see "Quiz Complete!", a "Try Again" button, and a blank area where the score will go.

Don't see the results section?

  • Make sure the results-screen div is inside the quiz-container div, not outside it.
  • Check that you didn't accidentally delete the quiz-container closing </div>. There should be one </div> closing the results-screen and another closing the quiz-container.

Help me check my HTML nesting is correct.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html before moving on to the CSS.

ⓧ I'd like to double check the full code

Here's what your complete index.html should look like at this point.

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JLPT N3 Grammar Quiz</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="quiz-container">
        <div class="quiz-header">
            <span id="question-counter">Question 1/12</span>
            <span id="score-display">Score: 0</span>
        </div>

        <div id="quiz-screen">
            <p id="question-text" class="question-text"></p>
            <div id="answers-container" class="answers-container">
                <button class="answer-btn" data-index="0"></button>
                <button class="answer-btn" data-index="1"></button>
                <button class="answer-btn" data-index="2"></button>
                <button class="answer-btn" data-index="3"></button>
            </div>
            <p id="explanation" class="explanation"></p>
            <button id="next-btn" class="next-btn">Next</button>
        </div>

        <div id="results-screen" class="results-screen hidden">
            <h2>Quiz Complete!</h2>
            <p id="final-score" class="final-score"></p>
            <p id="result-message" class="result-message"></p>
            <button id="try-again-btn" class="try-again-btn">Try Again</button>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>
Style the quiz with CSS

Right now your quiz is just unstyled text and buttons. You'll add CSS to create a dark-themed card layout, large tappable answer buttons, and color feedback classes that JavaScript will apply later.

  • Open style.css in VS Code.
  • Add the layout foundation styles by pasting this code:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #1a1a2e;
    color: #eee;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.quiz-container {
    background-color: #16213e;
    border-radius: 16px;
    padding: 40px;
    max-width: 600px;
    width: 100%;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

.quiz-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 30px;
    font-size: 14px;
    color: #a0a0b0;
}

.question-text {
    font-size: 20px;
    line-height: 1.6;
    margin-bottom: 30px;
    min-height: 60px;
}

What do these styles do?

  • The * selector resets all default margins and padding so spacing is consistent.
  • The body uses flexbox to center the quiz card vertically and horizontally on a dark navy background.
  • The .quiz-container creates a slightly lighter card with rounded corners and a subtle shadow.
  • Save style.css and refresh your browser.
  • You should see a dark navy page with a slightly lighter card in the center. The "Question 1/12" and "Score: 0" text should sit at the top of the card.

Page still looks unstyled?

  • Check that the <link rel="stylesheet" href="style.css"> line exists in your index.html head section.
  • Make sure the filename is exactly style.css (not styles.css or style.CSS). Filenames are case-sensitive.

Why isn't my CSS file being applied to the page?

  • Next, add the answer button and interactive element styles below what you just pasted:
.answers-container {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 20px;
}

.answer-btn {
    background-color: #0f3460;
    color: #eee;
    border: 2px solid #0f3460;
    border-radius: 10px;
    padding: 14px 20px;
    font-size: 16px;
    cursor: pointer;
    text-align: left;
    transition: background-color 0.2s, border-color 0.2s;
}

.answer-btn:hover {
    background-color: #1a4a7a;
    border-color: #1a4a7a;
}

.answer-btn:disabled {
    cursor: not-allowed;
}

.answer-btn.correct {
    background-color: #1b8a3a;
    border-color: #1b8a3a;
}

.answer-btn.incorrect {
    background-color: #8a1b1b;
    border-color: #8a1b1b;
}

.explanation {
    font-size: 14px;
    color: #a0d0a0;
    line-height: 1.5;
    margin-bottom: 20px;
    min-height: 40px;
}

.next-btn {
    background-color: #e94560;
    color: #fff;
    border: none;
    border-radius: 10px;
    padding: 12px 30px;
    font-size: 16px;
    cursor: pointer;
    display: none;
}

.next-btn:hover {
    background-color: #d63851;
}

What do the .correct and .incorrect classes do?

These classes aren't applied in the HTML. Later, your JavaScript will add .correct (green) or .incorrect (red) to a button after the learner clicks it. This gives instant visual feedback on whether the answer was right or wrong.

The .next-btn starts with display: none so it only appears after an answer is selected.

  • Save style.css and refresh your browser.
  • You should see four dark blue buttons stacked vertically. They're empty for now but have rounded corners and padding. Hover over one to see it lighten slightly.

Buttons not showing the hover effect?

  • Make sure the .answer-btn:hover selector has a colon before hover, not a dot or space.
  • Check that there are no typos in the class name. It must be answer-btn (with a hyphen), matching the HTML.

Help me debug why my CSS hover effect isn't working.

  • Finally, add the results screen and utility styles below your existing CSS:
.results-screen {
    text-align: center;
}

.results-screen h2 {
    font-size: 28px;
    margin-bottom: 20px;
}

.final-score {
    font-size: 48px;
    font-weight: bold;
    color: #e94560;
    margin-bottom: 10px;
}

.result-message {
    font-size: 16px;
    color: #a0a0b0;
    margin-bottom: 30px;
}

.try-again-btn {
    background-color: #e94560;
    color: #fff;
    border: none;
    border-radius: 10px;
    padding: 14px 40px;
    font-size: 16px;
    cursor: pointer;
}

.try-again-btn:hover {
    background-color: #d63851;
}

.hidden {
    display: none;
}

What does this code do?

  • .results-screen centers the final score and message text for a clean end-screen layout.
  • .final-score makes the score number large (48px) and bold in a pink accent color so it stands out.
  • .hidden is a utility class that hides any element. JavaScript will add and remove this class to switch between the quiz screen and results screen.
  • Save style.css and refresh your browser.
  • The "Quiz Complete!" heading and "Try Again" button should now disappear. The .hidden class is hiding the results screen as intended.

Still seeing Quiz Complete! on the page?

  • Check that the results-screen div in your HTML has both classes: class="results-screen hidden". The word hidden must be in the class attribute.
  • Make sure the .hidden rule is at the bottom of your CSS file and hasn't been accidentally placed inside another selector's curly braces.

Why isn't my hidden class hiding the results screen?

✔️ Awesome, I've got everything!

Great. Double check you've saved style.css before moving on.

ⓧ I'd like to double check the full code

Here's what your complete style.css should look like at this point.

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

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #1a1a2e;
    color: #eee;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.quiz-container {
    background-color: #16213e;
    border-radius: 16px;
    padding: 40px;
    max-width: 600px;
    width: 100%;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}

.quiz-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 30px;
    font-size: 14px;
    color: #a0a0b0;
}

.question-text {
    font-size: 20px;
    line-height: 1.6;
    margin-bottom: 30px;
    min-height: 60px;
}

.answers-container {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 20px;
}

.answer-btn {
    background-color: #0f3460;
    color: #eee;
    border: 2px solid #0f3460;
    border-radius: 10px;
    padding: 14px 20px;
    font-size: 16px;
    cursor: pointer;
    text-align: left;
    transition: background-color 0.2s, border-color 0.2s;
}

.answer-btn:hover {
    background-color: #1a4a7a;
    border-color: #1a4a7a;
}

.answer-btn:disabled {
    cursor: not-allowed;
}

.answer-btn.correct {
    background-color: #1b8a3a;
    border-color: #1b8a3a;
}

.answer-btn.incorrect {
    background-color: #8a1b1b;
    border-color: #8a1b1b;
}

.explanation {
    font-size: 14px;
    color: #a0d0a0;
    line-height: 1.5;
    margin-bottom: 20px;
    min-height: 40px;
}

.next-btn {
    background-color: #e94560;
    color: #fff;
    border: none;
    border-radius: 10px;
    padding: 12px 30px;
    font-size: 16px;
    cursor: pointer;
    display: none;
}

.next-btn:hover {
    background-color: #d63851;
}

.results-screen {
    text-align: center;
}

.results-screen h2 {
    font-size: 28px;
    margin-bottom: 20px;
}

.final-score {
    font-size: 48px;
    font-weight: bold;
    color: #e94560;
    margin-bottom: 10px;
}

.result-message {
    font-size: 16px;
    color: #a0a0b0;
    margin-bottom: 30px;
}

.try-again-btn {
    background-color: #e94560;
    color: #fff;
    border: none;
    border-radius: 10px;
    padding: 14px 40px;
    font-size: 16px;
    cursor: pointer;
}

.try-again-btn:hover {
    background-color: #d63851;
}

.hidden {
    display: none;
}

Your quiz interface is styled and ready. Next up, you'll create the actual grammar questions that power this quiz.

Create the Grammar Questions

Your quiz interface looks great, but it has no data to display yet. The quiz needs a set of grammar questions to pull from.

In this step, you'll create a JavaScript array of 12 real JLPT N3 grammar questions. Each question follows the same fill-in-the-blank format used on the actual test.

In this step, get ready to:
  • Create a questions data array in JavaScript.
  • Add 12 JLPT N3 grammar drill questions with answer options and explanations.
  • Verify the array loads without errors in the browser console.
Build the questions array

JavaScript uses arrays to store ordered lists of data. Each item in your array will be an object containing the question sentence, four answer choices, the index of the correct answer, and an explanation.

  • In VS Code, click on script.js in the Explorer sidebar to open it.
  • Paste the following questions array into the empty file:
const questions = [
    {
        question: "明日は晴れる__だったのに、雨が降っている。",
        options: ["はず", "つもり", "ため", "わけ"],
        correct: 0,
        explanation: "~はずだ = 'should be / expected to'. 明日は晴れるはずだった = It was supposed to be sunny tomorrow."
    },
    {
        question: "来月から毎日運動する__。",
        options: ["ことになった", "ことにした", "ようになった", "ことがある"],
        correct: 1,
        explanation: "~ことにする = 'decide to do'. 自分で決めた = You decided yourself to exercise every day."
    },
    {
        question: "来年から会社のルールが変わる__。",
        options: ["ことにした", "ことになった", "ようにした", "わけだ"],
        correct: 1,
        explanation: "~ことになる = 'it has been decided that'. The rule change was decided by others (the company), not by you."
    },
    {
        question: "日本語が上手になる__、毎日練習しています。",
        options: ["ために", "ように", "ことに", "ばかり"],
        correct: 1,
        explanation: "~ように = 'so that'. Used with potential/intransitive verbs. 上手になる is intransitive, so ように is correct."
    },
    {
        question: "家族を養う__、二つの仕事をしている。",
        options: ["ように", "ために", "からには", "一方で"],
        correct: 1,
        explanation: "~ために = 'in order to'. Used with volitional/transitive verbs. 養う (to support) is intentional action."
    },
    {
        question: "さっき昼ご飯を食べた__なのに、もうお腹がすいた。",
        options: ["ところ", "ばかり", "きり", "まま"],
        correct: 1,
        explanation: "V(た形) + ばかり = 'just did'. 食べたばかり = just ate (a short time ago)."
    },
    {
        question: "料理をしている__に、電話が来た。",
        options: ["ところ", "最中", "うちに", "ばかり"],
        correct: 1,
        explanation: "~最中に = 'in the very middle of'. Emphasizes interruption during an ongoing action."
    },
    {
        question: "彼は優しい__、厳しいところもある。",
        options: ["一方で", "からには", "ことから", "どころか"],
        correct: 0,
        explanation: "~一方で = 'on the other hand / while also'. Shows two contrasting qualities of the same subject."
    },
    {
        question: "約束した__、最後までやるべきだ。",
        options: ["ことから", "からには", "一方で", "次第"],
        correct: 1,
        explanation: "~からには = 'since / now that'. Implies strong resolve or obligation following a decision."
    },
    {
        question: "彼はいつも笑っている__、「スマイルさん」と呼ばれている。",
        options: ["からには", "一方で", "ことから", "次第"],
        correct: 2,
        explanation: "~ことから = 'from the fact that / because'. Used to explain the reason or origin of something (his nickname)."
    },
    {
        question: "届き__、ご連絡いたします。",
        options: ["ながら", "次第", "きり", "がち"],
        correct: 1,
        explanation: "V(ます形) + 次第 = 'as soon as'. Formal expression: I will contact you as soon as it arrives."
    },
    {
        question: "あの人の名前、何だ__?思い出せない。",
        options: ["っけ", "かな", "わけ", "はず"],
        correct: 0,
        explanation: "~っけ = 'what was it again?' Used when trying to recall something you once knew."
    }
];

What does this code do?

This creates a const variable called questions that holds an array (a list) of 12 objects. Each object has four properties:

  • question contains the sentence with __ marking where the grammar pattern goes.
  • options is an array of 4 possible answers.
  • correct is the index (starting from 0) of the right answer in the options array.
  • explanation gives the grammar rule in Japanese and English so you learn from each question.

Why these 12 grammar patterns?

These patterns cover the most important N3 grammar categories: expectation (はずだ), decisions (ことにする vs ことになる), purpose (ように vs ために), time (ばかり, 最中に), contrast (一方で), cause/reason (からには, ことから), sequence (次第), and recall (っけ).

The questions include tricky pairs that test-takers commonly confuse, like ために vs ように. One uses volitional verbs (intentional actions), while the other uses intransitive or potential verbs.

  • Save script.js.

You won't see a change in the browser yet. The array stores data but doesn't display anything on its own. The next substep confirms it loaded correctly.

Test that the questions load

Before building the quiz logic, you should confirm that the browser can read your questions array without any syntax errors. A quick console.log will tell you.

  • At the very bottom of script.js (after the closing ]; of the array), add this line:
console.log(questions.length);

What does this line do?

console.log() prints a value to the browser's developer console. questions.length returns how many items are in the array. If everything is correct, you'll see 12 printed in the console.

  • Save script.js.
  • Switch to your browser and refresh the page showing index.html.
  • Press F12 to open the developer tools.
  • Click the Console tab. You should see 12 printed there with no red error messages.

Seeing a red error instead of 12?

  • Check for missing commas between question objects. Each } that closes one question must be followed by a , before the next { (except the last one).
  • Look for unescaped quotes inside strings. The explanations use single quotes ('should be') inside double-quoted strings, which is fine. But an accidental double quote inside a double-quoted string will break things.
  • Make sure console.log(questions.length); is outside the array's closing ]; bracket, not inside it.

Still stuck? Help me debug my questions array in script.js

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete script.js file at the end of this step:

const questions = [
    {
        question: "明日は晴れる__だったのに、雨が降っている。",
        options: ["はず", "つもり", "ため", "わけ"],
        correct: 0,
        explanation: "~はずだ = 'should be / expected to'. 明日は晴れるはずだった = It was supposed to be sunny tomorrow."
    },
    {
        question: "来月から毎日運動する__。",
        options: ["ことになった", "ことにした", "ようになった", "ことがある"],
        correct: 1,
        explanation: "~ことにする = 'decide to do'. 自分で決めた = You decided yourself to exercise every day."
    },
    {
        question: "来年から会社のルールが変わる__。",
        options: ["ことにした", "ことになった", "ようにした", "わけだ"],
        correct: 1,
        explanation: "~ことになる = 'it has been decided that'. The rule change was decided by others (the company), not by you."
    },
    {
        question: "日本語が上手になる__、毎日練習しています。",
        options: ["ために", "ように", "ことに", "ばかり"],
        correct: 1,
        explanation: "~ように = 'so that'. Used with potential/intransitive verbs. 上手になる is intransitive, so ように is correct."
    },
    {
        question: "家族を養う__、二つの仕事をしている。",
        options: ["ように", "ために", "からには", "一方で"],
        correct: 1,
        explanation: "~ために = 'in order to'. Used with volitional/transitive verbs. 養う (to support) is intentional action."
    },
    {
        question: "さっき昼ご飯を食べた__なのに、もうお腹がすいた。",
        options: ["ところ", "ばかり", "きり", "まま"],
        correct: 1,
        explanation: "V(た形) + ばかり = 'just did'. 食べたばかり = just ate (a short time ago)."
    },
    {
        question: "料理をしている__に、電話が来た。",
        options: ["ところ", "最中", "うちに", "ばかり"],
        correct: 1,
        explanation: "~最中に = 'in the very middle of'. Emphasizes interruption during an ongoing action."
    },
    {
        question: "彼は優しい__、厳しいところもある。",
        options: ["一方で", "からには", "ことから", "どころか"],
        correct: 0,
        explanation: "~一方で = 'on the other hand / while also'. Shows two contrasting qualities of the same subject."
    },
    {
        question: "約束した__、最後までやるべきだ。",
        options: ["ことから", "からには", "一方で", "次第"],
        correct: 1,
        explanation: "~からには = 'since / now that'. Implies strong resolve or obligation following a decision."
    },
    {
        question: "彼はいつも笑っている__、「スマイルさん」と呼ばれている。",
        options: ["からには", "一方で", "ことから", "次第"],
        correct: 2,
        explanation: "~ことから = 'from the fact that / because'. Used to explain the reason or origin of something (his nickname)."
    },
    {
        question: "届き__、ご連絡いたします。",
        options: ["ながら", "次第", "きり", "がち"],
        correct: 1,
        explanation: "V(ます形) + 次第 = 'as soon as'. Formal expression: I will contact you as soon as it arrives."
    },
    {
        question: "あの人の名前、何だ__?思い出せない。",
        options: ["っけ", "かな", "わけ", "はず"],
        correct: 0,
        explanation: "~っけ = 'what was it again?' Used when trying to recall something you once knew."
    }
];

console.log(questions.length);

Your quiz data is loaded and ready. Next up, you'll write the JavaScript functions that pull questions from this array and display them on screen.

Wire Up the Quiz Logic

Your quiz has a polished interface and 12 grammar questions stored in an array. What it doesn't have yet is a brain.

In this step, you'll write the JavaScript logic that pulls questions out of the array, displays them on screen, and responds when the learner clicks an answer.

In this step, get ready to:
  • Write a loadQuestion() function that populates the quiz screen.
  • Add event listeners so answer buttons respond to clicks.
  • Wire up the Next button to advance through questions.
Write the loadQuestion function

Before any function can update the page, JavaScript needs a way to reach the HTML elements. You do this by storing references to each element in variables using DOM methods like getElementById.

  • In script.js, find and delete the temporary console.log(questions.length); line at the bottom of the file.
  • Replace it with these state variables and DOM references by pasting this code below the questions array:
let currentQuestionIndex = 0;
let score = 0;
let shuffledQuestions = [];

const questionCounter = document.getElementById("question-counter");
const scoreDisplay = document.getElementById("score-display");
const questionText = document.getElementById("question-text");
const answersContainer = document.getElementById("answers-container");
const answerButtons = document.querySelectorAll(".answer-btn");
const explanation = document.getElementById("explanation");
const nextBtn = document.getElementById("next-btn");
const quizScreen = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const finalScore = document.getElementById("final-score");
const resultMessage = document.getElementById("result-message");
const tryAgainBtn = document.getElementById("try-again-btn");

What does this code do?

The three let variables track the quiz state: which question you're on, your score, and the shuffled question order.

The const lines grab references to every HTML element the quiz needs to update. Each ID (like question-counter) matches an id attribute you set in index.html.

Now you have handles to every element. Next, add the functions that use them.

  • Add the shuffle, start, and load functions below the DOM references by pasting this code at the bottom of script.js:
function shuffleArray(array) {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
}

function startQuiz() {
    currentQuestionIndex = 0;
    score = 0;
    shuffledQuestions = shuffleArray(questions);
    scoreDisplay.textContent = "Score: 0";
    quizScreen.classList.remove("hidden");
    resultsScreen.classList.add("hidden");
    loadQuestion();
}

function loadQuestion() {
    const current = shuffledQuestions[currentQuestionIndex];
    questionCounter.textContent = `Question ${currentQuestionIndex + 1}/${shuffledQuestions.length}`;
    questionText.textContent = current.question;
    explanation.textContent = "";
    nextBtn.style.display = "none";

    answerButtons.forEach((btn, index) => {
        btn.textContent = current.options[index];
        btn.classList.remove("correct", "incorrect");
        btn.disabled = false;
    });
}

startQuiz();

What does this code do?

  • shuffleArray() uses the Fisher-Yates algorithm to randomize the question order. It creates a copy first so the original array stays untouched.
  • startQuiz() resets the score and question index to zero, shuffles the questions, and calls loadQuestion() to display the first one.
  • loadQuestion() reads the current question from the shuffled array and writes its sentence into the question area. It also fills the four answer buttons with that question's options.
  • startQuiz(); at the bottom runs everything as soon as the page loads.
  • Save script.js.
  • Refresh your browser. You should see a Japanese sentence with a blank (__) and four grammar pattern options below it.

Don't see a question?

  • Open DevTools with F12 and check the Console tab for red error text. A typo in an element ID (like question-Text instead of question-text) will cause a null reference error.
  • Make sure you deleted the console.log(questions.length); line. It won't break anything, but confirm it's gone so your file stays clean.

Why isn't my quiz displaying the first question?

Add answer button event listeners

The buttons display options, but clicking them does nothing yet. You need an event listener on each button that checks the answer and gives visual feedback.

  • In script.js, find the startQuiz(); line at the very bottom of the file.
  • Add the selectAnswer function and the button event listeners directly ABOVE that startQuiz(); call:
function selectAnswer(selectedIndex) {
    const current = shuffledQuestions[currentQuestionIndex];

    answerButtons.forEach((btn) => {
        btn.disabled = true;
    });

    if (selectedIndex === current.correct) {
        answerButtons[selectedIndex].classList.add("correct");
        score++;
        scoreDisplay.textContent = `Score: ${score}`;
    } else {
        answerButtons[selectedIndex].classList.add("incorrect");
        answerButtons[current.correct].classList.add("correct");
    }

    explanation.textContent = current.explanation;
    nextBtn.style.display = "inline-block";
}

answerButtons.forEach((btn) => {
    btn.addEventListener("click", () => {
        const selectedIndex = parseInt(btn.getAttribute("data-index"));
        selectAnswer(selectedIndex);
    });
});

What does this code do?

  • selectAnswer() first disables all four buttons so you can't click twice. Then it compares your pick against current.correct and adds a CSS class: .correct (green) for the right answer, .incorrect (red) for a wrong pick.
  • If you're right, score goes up and the header updates. If you're wrong, both your wrong choice (red) and the correct answer (green) are highlighted.
  • The grammar explanation appears, and the Next button becomes visible so you can advance.
  • The addEventListener loop attaches a click handler to each button. It reads the data-index attribute to know which option (0-3) was clicked.
  • Save script.js and refresh your browser.
  • Click any answer button. You should see the button turn green (if correct) or red (if wrong), the correct answer highlighted green, the grammar explanation text appear below the buttons, and the Next button appear.

Buttons not responding to clicks?

  • Check that the answerButtons.forEach event listener block is placed ABOVE the startQuiz(); call, not below it.
  • Make sure answerButtons uses querySelectorAll(".answer-btn") with the dot before the class name. Missing the dot returns nothing.

Help me debug why my answer buttons aren't responding to clicks.

Wire up the Next button

You can answer one question, but the quiz is stuck there. The Next button needs logic to advance to the next question or end the quiz.

  • In script.js, find the startQuiz(); line at the bottom again.
  • Add the Next button and Try Again button listeners directly ABOVE startQuiz();:
nextBtn.addEventListener("click", () => {
    currentQuestionIndex++;
    if (currentQuestionIndex < shuffledQuestions.length) {
        loadQuestion();
    } else {
        showResults();
    }
});

tryAgainBtn.addEventListener("click", startQuiz);

What does this code do?

  • The Next button listener increments currentQuestionIndex. If there are more questions, it calls loadQuestion() to show the next one. Otherwise it calls showResults() (which you'll write in the next step).
  • The Try Again button simply calls startQuiz to reset everything and start fresh.
  • Save script.js and refresh your browser.
  • Answer a question, then click the Next button. A new question with different answer options should appear.
  • Try clicking through several questions to confirm the counter updates (Question 2/12, Question 3/12, etc.).

What about showResults?

You'll write the showResults() function in the next step. For now, if you click through all 12 questions, you'll see an error in the console. That's expected and you'll fix it shortly.

Next button not advancing?

  • Make sure the Next button listener uses the ID next-btn (with a hyphen), matching the id in your HTML.
  • Confirm the listener is placed above the startQuiz(); call at the bottom. If it's below, the quiz starts before the listener is registered.

Why isn't my Next button loading the next question?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete script.js as it should look now:

const questions = [
    {
        question: "明日は晴れる__だったのに、雨が降っている。",
        options: ["はず", "つもり", "ため", "わけ"],
        correct: 0,
        explanation: "~はずだ = 'should be / expected to'. 明日は晴れるはずだった = It was supposed to be sunny tomorrow."
    },
    {
        question: "来月から毎日運動する__。",
        options: ["ことになった", "ことにした", "ようになった", "ことがある"],
        correct: 1,
        explanation: "~ことにする = 'decide to do'. 自分で決めた = You decided yourself to exercise every day."
    },
    {
        question: "来年から会社のルールが変わる__。",
        options: ["ことにした", "ことになった", "ようにした", "わけだ"],
        correct: 1,
        explanation: "~ことになる = 'it has been decided that'. The rule change was decided by others (the company), not by you."
    },
    {
        question: "日本語が上手になる__、毎日練習しています。",
        options: ["ために", "ように", "ことに", "ばかり"],
        correct: 1,
        explanation: "~ように = 'so that'. Used with potential/intransitive verbs. 上手になる is intransitive, so ように is correct."
    },
    {
        question: "家族を養う__、二つの仕事をしている。",
        options: ["ように", "ために", "からには", "一方で"],
        correct: 1,
        explanation: "~ために = 'in order to'. Used with volitional/transitive verbs. 養う (to support) is intentional action."
    },
    {
        question: "さっき昼ご飯を食べた__なのに、もうお腹がすいた。",
        options: ["ところ", "ばかり", "きり", "まま"],
        correct: 1,
        explanation: "V(た形) + ばかり = 'just did'. 食べたばかり = just ate (a short time ago)."
    },
    {
        question: "料理をしている__に、電話が来た。",
        options: ["ところ", "最中", "うちに", "ばかり"],
        correct: 1,
        explanation: "~最中に = 'in the very middle of'. Emphasizes interruption during an ongoing action."
    },
    {
        question: "彼は優しい__、厳しいところもある。",
        options: ["一方で", "からには", "ことから", "どころか"],
        correct: 0,
        explanation: "~一方で = 'on the other hand / while also'. Shows two contrasting qualities of the same subject."
    },
    {
        question: "約束した__、最後までやるべきだ。",
        options: ["ことから", "からには", "一方で", "次第"],
        correct: 1,
        explanation: "~からには = 'since / now that'. Implies strong resolve or obligation following a decision."
    },
    {
        question: "彼はいつも笑っている__、「スマイルさん」と呼ばれている。",
        options: ["からには", "一方で", "ことから", "次第"],
        correct: 2,
        explanation: "~ことから = 'from the fact that / because'. Used to explain the reason or origin of something (his nickname)."
    },
    {
        question: "届き__、ご連絡いたします。",
        options: ["ながら", "次第", "きり", "がち"],
        correct: 1,
        explanation: "V(ます形) + 次第 = 'as soon as'. Formal expression: I will contact you as soon as it arrives."
    },
    {
        question: "あの人の名前、何だ__?思い出せない。",
        options: ["っけ", "かな", "わけ", "はず"],
        correct: 0,
        explanation: "~っけ = 'what was it again?' Used when trying to recall something you once knew."
    }
];

let currentQuestionIndex = 0;
let score = 0;
let shuffledQuestions = [];

const questionCounter = document.getElementById("question-counter");
const scoreDisplay = document.getElementById("score-display");
const questionText = document.getElementById("question-text");
const answersContainer = document.getElementById("answers-container");
const answerButtons = document.querySelectorAll(".answer-btn");
const explanation = document.getElementById("explanation");
const nextBtn = document.getElementById("next-btn");
const quizScreen = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const finalScore = document.getElementById("final-score");
const resultMessage = document.getElementById("result-message");
const tryAgainBtn = document.getElementById("try-again-btn");

function shuffleArray(array) {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
}

function startQuiz() {
    currentQuestionIndex = 0;
    score = 0;
    shuffledQuestions = shuffleArray(questions);
    scoreDisplay.textContent = "Score: 0";
    quizScreen.classList.remove("hidden");
    resultsScreen.classList.add("hidden");
    loadQuestion();
}

function loadQuestion() {
    const current = shuffledQuestions[currentQuestionIndex];
    questionCounter.textContent = `Question ${currentQuestionIndex + 1}/${shuffledQuestions.length}`;
    questionText.textContent = current.question;
    explanation.textContent = "";
    nextBtn.style.display = "none";

    answerButtons.forEach((btn, index) => {
        btn.textContent = current.options[index];
        btn.classList.remove("correct", "incorrect");
        btn.disabled = false;
    });
}

function selectAnswer(selectedIndex) {
    const current = shuffledQuestions[currentQuestionIndex];

    answerButtons.forEach((btn) => {
        btn.disabled = true;
    });

    if (selectedIndex === current.correct) {
        answerButtons[selectedIndex].classList.add("correct");
        score++;
        scoreDisplay.textContent = `Score: ${score}`;
    } else {
        answerButtons[selectedIndex].classList.add("incorrect");
        answerButtons[current.correct].classList.add("correct");
    }

    explanation.textContent = current.explanation;
    nextBtn.style.display = "inline-block";
}

answerButtons.forEach((btn) => {
    btn.addEventListener("click", () => {
        const selectedIndex = parseInt(btn.getAttribute("data-index"));
        selectAnswer(selectedIndex);
    });
});

nextBtn.addEventListener("click", () => {
    currentQuestionIndex++;
    if (currentQuestionIndex < shuffledQuestions.length) {
        loadQuestion();
    } else {
        showResults();
    }
});

tryAgainBtn.addEventListener("click", startQuiz);

startQuiz();

Your quiz is playable. Questions appear in a random order, answers give instant color feedback, and the Next button advances through the deck. Next up, you'll build the results screen that shows the final score when all 12 questions are done.

Add Score Tracking and Results

Your quiz can display questions, check answers, and show color feedback. But after answering the last question, nothing visible happens because the function that shows the results screen hasn't been written yet.

In this step, you'll add the missing piece: a results screen that displays your final score with a performance message based on how well you did.

In this step, get ready to:
  • Confirm your running score updates correctly as you answer questions.
  • Write the results screen function to display your final score and a performance message.
  • Verify that question shuffling gives you a different quiz each time.
Confirm your score is tracking

The score tracking logic is already wired up in your selectAnswer function from the previous step. Let's confirm it works correctly.

  • Refresh index.html in your browser.
  • Answer 2-3 questions correctly and watch the top-right corner of the quiz.

You should see the Score: 0 counter update to Score: 1, Score: 2, and so on each time you pick the correct answer.

How does score tracking work?

Inside selectAnswer(), the if (selectedIndex === current.correct) check runs score++ when you pick the right answer. It then updates the header display with the new value.

When you get a question wrong, the score stays the same. Only correct answers count.

Score not updating?

Make sure you saved script.js after the previous step. If the console (F12) shows any errors, fix them first.

help me debug why my score counter isn't updating when I answer correctly

Write the showResults function

Right now, your Next button listener calls showResults() after the last question, but that function doesn't exist yet. Time to write it.

  • In script.js, find the closing brace of your selectAnswer function.
  • Add the following function directly below it:
function showResults() {
    // Hide the quiz and show the results screen
    quizScreen.classList.add("hidden");
    resultsScreen.classList.remove("hidden");
    // Display final score as "X/12" format
    finalScore.textContent = `${score}/${shuffledQuestions.length}`;

    // Show a message based on how well the learner did
    if (score === shuffledQuestions.length) {
        resultMessage.textContent = "Perfect! You're ready for N3! 完璧!";
    } else if (score >= shuffledQuestions.length * 0.7) {
        resultMessage.textContent = "Great job! Review the patterns you missed. いい調子!";
    } else {
        resultMessage.textContent = "Keep studying! Focus on the grammar explanations. 頑張って!";
    }
}

What does this code do?

This function runs after the learner answers the final question and clicks Next.

  • It hides the quiz screen and reveals the results screen using the hidden CSS class.
  • It sets the big score display to show something like 8/12.
  • It picks a performance message based on three tiers: perfect score, 70% or above, or below 70%.
  • Save script.js.
  • Refresh your browser and play through all 12 questions.
  • After you answer the last question and click Next, you should see the results screen with your score in large red text, a performance message below it, and a Try Again button.

Results screen not appearing?

Open the browser console with F12 and look for errors. A common cause is placing showResults() below the event listeners that call it. Make sure it sits between selectAnswer and the answerButtons.forEach block.

Also check that you haven't accidentally deleted the closing brace of selectAnswer. Each function needs its own opening and closing }.

help me figure out why the results screen isn't showing after the last question

  • Click Try Again on the results screen.
  • The quiz should reset back to question 1 with your score at 0.

How does Try Again work?

The tryAgainBtn listener calls startQuiz(), which resets score to 0, reshuffles the questions into a fresh order, hides the results screen, shows the quiz screen, and loads question 1.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js before moving on.

ⓧ I'd like to double check the full code

const questions = [
    {
        question: "明日は晴れる__だったのに、雨が降っている。",
        options: ["はず", "つもり", "ため", "わけ"],
        correct: 0,
        explanation: "~はずだ = 'should be / expected to'. 明日は晴れるはずだった = It was supposed to be sunny tomorrow."
    },
    {
        question: "来月から毎日運動する__。",
        options: ["ことになった", "ことにした", "ようになった", "ことがある"],
        correct: 1,
        explanation: "~ことにする = 'decide to do'. 自分で決めた = You decided yourself to exercise every day."
    },
    {
        question: "来年から会社のルールが変わる__。",
        options: ["ことにした", "ことになった", "ようにした", "わけだ"],
        correct: 1,
        explanation: "~ことになる = 'it has been decided that'. The rule change was decided by others (the company), not by you."
    },
    {
        question: "日本語が上手になる__、毎日練習しています。",
        options: ["ために", "ように", "ことに", "ばかり"],
        correct: 1,
        explanation: "~ように = 'so that'. Used with potential/intransitive verbs. 上手になる is intransitive, so ように is correct."
    },
    {
        question: "家族を養う__、二つの仕事をしている。",
        options: ["ように", "ために", "からには", "一方で"],
        correct: 1,
        explanation: "~ために = 'in order to'. Used with volitional/transitive verbs. 養う (to support) is intentional action."
    },
    {
        question: "さっき昼ご飯を食べた__なのに、もうお腹がすいた。",
        options: ["ところ", "ばかり", "きり", "まま"],
        correct: 1,
        explanation: "V(た形) + ばかり = 'just did'. 食べたばかり = just ate (a short time ago)."
    },
    {
        question: "料理をしている__に、電話が来た。",
        options: ["ところ", "最中", "うちに", "ばかり"],
        correct: 1,
        explanation: "~最中に = 'in the very middle of'. Emphasizes interruption during an ongoing action."
    },
    {
        question: "彼は優しい__、厳しいところもある。",
        options: ["一方で", "からには", "ことから", "どころか"],
        correct: 0,
        explanation: "~一方で = 'on the other hand / while also'. Shows two contrasting qualities of the same subject."
    },
    {
        question: "約束した__、最後までやるべきだ。",
        options: ["ことから", "からには", "一方で", "次第"],
        correct: 1,
        explanation: "~からには = 'since / now that'. Implies strong resolve or obligation following a decision."
    },
    {
        question: "彼はいつも笑っている__、「スマイルさん」と呼ばれている。",
        options: ["からには", "一方で", "ことから", "次第"],
        correct: 2,
        explanation: "~ことから = 'from the fact that / because'. Used to explain the reason or origin of something (his nickname)."
    },
    {
        question: "届き__、ご連絡いたします。",
        options: ["ながら", "次第", "きり", "がち"],
        correct: 1,
        explanation: "V(ます形) + 次第 = 'as soon as'. Formal expression: I will contact you as soon as it arrives."
    },
    {
        question: "あの人の名前、何だ__?思い出せない。",
        options: ["っけ", "かな", "わけ", "はず"],
        correct: 0,
        explanation: "~っけ = 'what was it again?' Used when trying to recall something you once knew."
    }
];

let currentQuestionIndex = 0;
let score = 0;
let shuffledQuestions = [];

const questionCounter = document.getElementById("question-counter");
const scoreDisplay = document.getElementById("score-display");
const questionText = document.getElementById("question-text");
const answersContainer = document.getElementById("answers-container");
const answerButtons = document.querySelectorAll(".answer-btn");
const explanation = document.getElementById("explanation");
const nextBtn = document.getElementById("next-btn");
const quizScreen = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const finalScore = document.getElementById("final-score");
const resultMessage = document.getElementById("result-message");
const tryAgainBtn = document.getElementById("try-again-btn");

function shuffleArray(array) {
    const shuffled = [...array];
    for (let i = shuffled.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
}

function startQuiz() {
    currentQuestionIndex = 0;
    score = 0;
    shuffledQuestions = shuffleArray(questions);
    scoreDisplay.textContent = "Score: 0";
    quizScreen.classList.remove("hidden");
    resultsScreen.classList.add("hidden");
    loadQuestion();
}

function loadQuestion() {
    const current = shuffledQuestions[currentQuestionIndex];
    questionCounter.textContent = `Question ${currentQuestionIndex + 1}/${shuffledQuestions.length}`;
    questionText.textContent = current.question;
    explanation.textContent = "";
    nextBtn.style.display = "none";

    answerButtons.forEach((btn, index) => {
        btn.textContent = current.options[index];
        btn.classList.remove("correct", "incorrect");
        btn.disabled = false;
    });
}

function selectAnswer(selectedIndex) {
    const current = shuffledQuestions[currentQuestionIndex];

    answerButtons.forEach((btn) => {
        btn.disabled = true;
    });

    if (selectedIndex === current.correct) {
        answerButtons[selectedIndex].classList.add("correct");
        score++;
        scoreDisplay.textContent = `Score: ${score}`;
    } else {
        answerButtons[selectedIndex].classList.add("incorrect");
        answerButtons[current.correct].classList.add("correct");
    }

    explanation.textContent = current.explanation;
    nextBtn.style.display = "inline-block";
}

function showResults() {
    quizScreen.classList.add("hidden");
    resultsScreen.classList.remove("hidden");
    finalScore.textContent = `${score}/${shuffledQuestions.length}`;

    if (score === shuffledQuestions.length) {
        resultMessage.textContent = "Perfect! You're ready for N3! 完璧!";
    } else if (score >= shuffledQuestions.length * 0.7) {
        resultMessage.textContent = "Great job! Review the patterns you missed. いい調子!";
    } else {
        resultMessage.textContent = "Keep studying! Focus on the grammar explanations. 頑張って!";
    }
}

answerButtons.forEach((btn) => {
    btn.addEventListener("click", () => {
        const selectedIndex = parseInt(btn.getAttribute("data-index"));
        selectAnswer(selectedIndex);
    });
});

nextBtn.addEventListener("click", () => {
    currentQuestionIndex++;
    if (currentQuestionIndex < shuffledQuestions.length) {
        loadQuestion();
    } else {
        showResults();
    }
});

tryAgainBtn.addEventListener("click", startQuiz);

startQuiz();
Verify question shuffling

The Fisher-Yates shuffle algorithm in your shuffleArray function randomizes the question order every time the quiz starts. Let's confirm it works.

  • Play through the full quiz until you see the results screen.
  • Click Try Again.
  • Note the first question that appears. It should be different from the first question you saw last time.

Each time you restart, the questions arrive in a new random order. This prevents you from memorizing answers by position.

What is Fisher-Yates shuffle?

Fisher-Yates walks backward through the array, swapping each element with a randomly chosen element before it. This produces a truly uniform random order where every possible arrangement is equally likely.

Your startQuiz() function calls shuffleArray(questions) every time it runs. That includes the initial load and every Try Again click.

Secret mission

Add a Review Mistakes Mode

Your quiz tracks your score, but what about the patterns you got wrong? Right now, those mistakes vanish when you hit Try Again. What if you could drill only the questions you missed until you get them all right?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue adding questions, or delete them if you're done.

Resources you used:

  • The n3-quiz folder on your Desktop (contains index.html, style.css, and script.js).

Keep everything running

No action needed. Choose this if you plan to keep adding grammar questions to your quiz as you study for the July test.

  • Your n3-quiz folder stays on your Desktop. Open index.html in your browser anytime to practice.
  • To add new questions, open script.js in VS Code and add new objects to the questions array.

Pause - I'll come back to this later

Since this project is just local files, there is nothing to pause or shut down. Your files will be waiting for you whenever you come back.

  • Close the n3-quiz folder in VS Code by clicking File, then Close Folder.
  • Close any browser tabs with the quiz open.
  • Your project files remain on your Desktop, ready to reopen anytime.

Delete - I don't want to use this again

Remove all project files from your Desktop. This cannot be undone.

  • Close the n3-quiz folder in VS Code by clicking File, then Close Folder.
  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/n3-quiz

Windows

rmdir /s /q %USERPROFILE%\Desktop\n3-quiz
  • Confirm the folder is gone by checking your Desktop.

Nice Work!

Nice Work!

Nice work! You just built your own JLPT N3 grammar study tool from scratch using HTML, CSS, and JavaScript.

You've learned how to:

  • Built a complete quiz app with HTML, CSS, and JavaScript working together as a single-page application.
  • Used DOM manipulation, event listeners, and array methods to create interactive browser logic.
  • Created 12 real JLPT N3 grammar drill questions in the same fill-in-the-blank format as the actual test.
  • Added a Review Mistakes mode that targets your weak grammar patterns for focused practice.

Ready to quiz yourself?