HTML quiz code with score

One file, one script, and a score that means something. Counting correct answers is the small part; the useful part is showing which ones were wrong and what the score band implies.

HTML quiz code with score needs four things beyond a plain quiz: a per question record of what was right, a total, a percentage band, and a way to start again.

Below is the complete file. It runs with no server, no build step and no library.

A completed quiz showing the score line, the band and each question marked.
A completed quiz showing the score line, the band and each question marked.

The complete HTML quiz code with score

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quiz</title>
<style>
  body{font-family:system-ui,sans-serif;max-width:640px;margin:32px auto;
       padding:0 16px;color:#111;}
  fieldset{border:1px solid #ddd;border-radius:6px;margin:0 0 16px;padding:12px 16px;}
  legend{font-weight:600;padding:0 6px;}
  label{display:block;padding:6px 0;cursor:pointer;}
  .mark{font-size:13px;margin-top:6px;}
  .ok{color:#1a7f37;} .no{color:#b3261e;}
  #panel{display:none;border-top:2px solid #111;padding-top:16px;}
  button{font-size:15px;padding:10px 18px;}
</style>
</head>
<body>
<h1>Quick check</h1>
<form id="quiz"></form>
<button id="check" type="button">Check answers</button>
<div id="panel">
  <p id="score"></p>
  <p id="band"></p>
  <button id="again" type="button">Try again</button>
</div>
<script>
var questions = [
  { q: 'Which attribute gives an image its text alternative?',
    options: ['title', 'alt', 'caption'], answer: 1 },
  { q: 'Which element groups related form controls?',
    options: ['section', 'fieldset', 'div'], answer: 1 },
  { q: 'Which unit is safest in an email template?',
    options: ['rem', 'vw', 'px'], answer: 2 },
  { q: 'What does a share link give a page?',
    options: ['A download', 'An address', 'A screenshot'], answer: 1 }
];

var form = document.getElementById('quiz');
questions.forEach(function (item, i) {
  var fs = document.createElement('fieldset');
  var html = '<legend>' + item.q + '</legend>';
  item.options.forEach(function (opt, j) {
    var id = 'q' + i + 'o' + j;
    html += '<label for="' + id + '"><input type="radio" id="' + id +
            '" name="q' + i + '" value="' + j + '"> ' + opt + '</label>';
  });
  html += '<div class="mark" id="m' + i + '"></div>';
  fs.innerHTML = html;
  form.appendChild(fs);
});

function bandFor(pct) {
  if (pct === 100) return 'All correct. Nothing to revisit.';
  if (pct >= 75) return 'Solid. Read back the ones marked incorrect.';
  if (pct >= 50) return 'Half there. Worth a second pass over the material.';
  return 'Start again from the beginning of the guide.';
}

document.getElementById('check').onclick = function () {
  var right = 0;
  questions.forEach(function (item, i) {
    var picked = document.querySelector('input[name="q' + i + '"]:checked');
    var mark = document.getElementById('m' + i);
    if (!picked) {
      mark.className = 'mark no';
      mark.textContent = 'Not answered. Correct answer: ' + item.options[item.answer];
      return;
    }
    if (Number(picked.value) === item.answer) {
      right++;
      mark.className = 'mark ok';
      mark.textContent = 'Correct';
    } else {
      mark.className = 'mark no';
      mark.textContent = 'Incorrect. Correct answer: ' + item.options[item.answer];
    }
  });
  var pct = Math.round((right / questions.length) * 100);
  document.getElementById('score').textContent =
    'Score: ' + right + ' of ' + questions.length + ' (' + pct + '%)';
  document.getElementById('band').textContent = bandFor(pct);
  document.getElementById('panel').style.display = 'block';
  document.getElementById('panel').scrollIntoView();
};

document.getElementById('again').onclick = function () {
  form.reset();
  questions.forEach(function (_, i) {
    var mark = document.getElementById('m' + i);
    mark.textContent = '';
    mark.className = 'mark';
  });
  document.getElementById('panel').style.display = 'none';
  window.scrollTo(0, 0);
};
</script>
</body>
</html>

What the code decides for you

Four choices are made in that file, and each is worth knowing about because you may want the opposite.

Decision This file does The alternative
Unanswered questions Counted as incorrect Block submission until all are answered
When marking appears All at once after submit Immediately per question
Correct answers Revealed after checking Hidden, only the score shown
Retry Clears everything Keeps the best score

Change bandFor first. The bands are the part readers actually respond to, and generic praise is worse than no message.

A question marked incorrect, with the correct answer named underneath.
A question marked incorrect, with the correct answer named underneath.

Reading the script in four parts

The file is short enough to hold in your head, and it splits cleanly.

The data. One array, one object per question, each with the prompt, the options and the index of the correct option. Adding a question is one line, and no markup changes.

The render loop. It builds a fieldset per question and appends it. Each option gets its own id, and the label points at that id, so the whole option text is clickable.

The check handler. It walks the questions once, finds the checked input for each group, increments the counter and writes the per question mark at the same time.

The reset handler. It clears the form, empties the marks and hides the result panel. Without it, a second attempt shows stale marking next to fresh answers.

Keeping the counting and the marking in one pass is deliberate. Two separate loops drift apart the moment someone adds partial credit or a skipped question rule.

Percentages and rounding

Divide the count by the number of questions, then round once at the point of display.

Rounding earlier causes the band comparison to use a number the reader never sees, which produces the odd case of a page showing 75 percent while giving the message for the band below it.

If the quiz has a pass mark, state it on the page before the taker starts. A score that arrives with an unannounced threshold reads as arbitrary.

Marking with words, not only colour

Green and red are the obvious signal, and they are not enough on their own.

The file writes the word Correct or Incorrect into each mark element. That survives colour blindness, printing, and a reader who has the page in a high contrast mode.

Naming the correct answer in the same line is also what turns a score into something useful. A bare number tells the taker nothing about what to read again.

Remembering the last attempt

If you want the page to recall a previous score on the same device, local storage holds it.

localStorage.setItem('quizBest', String(Math.max(pct, Number(localStorage.getItem('quizBest') || 0))));

Be honest about what that is. It is per device and per browser, the user can clear it, and it is not a record you can rely on. Use it for a friendly best score line, not for tracking completion.

Recording who scored what

Marking in the browser means nothing leaves the page. If completion has to be recorded, the score has to be submitted.

The smallest version is a form below the result panel with a name field and a hidden input holding the score, posting to a form service you already run. Form to link covers the sharing and collection side of that.

Remember that the answer key sits in the source of the page. A submitted score can be edited by anyone who looks. For a knowledge check that is acceptable; for anything graded it is not.

Putting it where people can take it

Sending the file as an attachment runs into the usual problems. It gets filtered, it will not open on a phone, and each copy is frozen with whatever mistakes it had.

Paste the file into a NOS document instead. It renders as written, script included, and the quiz runs for whoever opens the link.

Creating the share link for the quiz document. Unlisted unless you make it public.
Creating the share link for the quiz document. Unlisted unless you make it public.

The address holds when you edit, so correcting a wrong key or rewording a question does not invalidate the link you already circulated.

Changes people usually want next

Four common extensions, none of them large.

Randomised order. Shuffle the questions array before rendering. Keep the answer index inside the object so shuffling cannot desynchronise the key.

Weighted questions. Add a points value per question and sum that instead of counting. The percentage then divides by the total points available.

One question at a time. Render a single fieldset and advance on answer. This suits longer quizzes, and it needs a progress line so the taker knows how far in they are.

Immediate feedback. Move the marking into a change handler on each group rather than into the submit button. Decide first whether you want the taker to change an answer after seeing it marked.

Accessibility of the result

The score panel appears after a click, and appearing content is easy to miss.

The file above calls scrollIntoView so the panel is brought into view. For screen reader users, an aria-live attribute on the panel announces the score without moving focus, which is usually the better behaviour.

Do not rely on the colours alone. The words Correct and Incorrect, plus the named correct answer, carry the whole meaning if every style is stripped away.

For the structure behind this file and the accessibility rules, see HTML quiz. If the quiz leads into a form, HTML signup page covers what happens next.

Questions people ask

How do I calculate a score in an HTML quiz?

Count the questions where the checked radio value matches the answer key, then divide by the number of questions for a percentage. Round once at display time rather than storing a rounded number, so the band comparison uses the exact value.

How do I show which answers were wrong?

Keep the per question outcome while you are counting, then loop again and mark each fieldset. Use a word as well as a colour, because colour alone is not readable for everyone and does not survive a printout.

Can the quiz remember a score between visits?

Local storage in the browser can hold the last score on that device. It is not a record of anything, since the user can clear it and it does not travel between devices, but it is enough to show a best attempt or to stop repeat prompts.

How do I share the quiz so people can take it?

Paste the whole file into a NOS document and create a share link. The page renders as written with the script running, so anyone with the address can take the quiz on a phone or a laptop with no download.

Keep reading