Before this lesson
Understand arrays, object state, event listeners and conditional rendering.
What you will be able to do
Build selection, grading, explanation, progression and restart, with one score update per question.
Understand the mechanism
Model unanswered, answered and finished states. index selects a question, score counts correct answers and answered prevents duplicate scoring. Store prompt, options, answer index and explanation as data, then render from it.
Use fieldset and legend to group radio choices and required for missing answers. After grading, lock choices, explain the result and expose progression. The handler also checks answered so correctness does not depend solely on a disabled button.
On each render, reset answered, feedback, progression visibility and disabled state. After the last question, hide the form and display the total. Restart resets index and score, not merely the displayed question.
Keyboard focus follows the workflow: progression after grading, first option on a new question and restart at completion. Answers are public in frontend code, making this suitable for self-study rather than secure assessment.
Runnable example
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web quiz</title><style>body{max-width:650px;margin:auto;padding:24px;font:18px/1.6 system-ui}label{display:block;padding:8px}button{font:inherit;margin:8px 4px}fieldset{min-width:0}</style>
<main><h1>Web foundations quiz</h1><p id="progress"></p><form id="quiz"><fieldset><legend id="question"></legend><div id="choices"></div></fieldset><button id="submit">Check answer</button></form><p id="feedback" role="status"></p><button id="next" hidden>Next question</button><button id="restart">Restart</button></main>
<script>
const questions=[{text:'Which element performs an action?',options:['button','a','span'],answer:0,why:'A native button supports keyboard activation.'},{text:'Which property allows flex items to wrap?',options:['position','flex-wrap','opacity'],answer:1,why:'flex-wrap permits additional flex lines.'},{text:'Which method inserts plain text?',options:['innerHTML','eval','textContent'],answer:2,why:'textContent treats input as literal text.'}];
let index=0,score=0,answered=false;
const form=document.querySelector('#quiz'),choices=document.querySelector('#choices'),feedback=document.querySelector('#feedback'),next=document.querySelector('#next'),submit=document.querySelector('#submit');
function render(){
answered=false;feedback.textContent='';next.hidden=true;form.hidden=false;submit.disabled=false;
const q=questions[index];document.querySelector('#progress').textContent=`Question ${index+1} of ${questions.length}`;document.querySelector('#question').textContent=q.text;choices.replaceChildren();
q.options.forEach((text,i)=>{const label=document.createElement('label'),input=document.createElement('input');input.type='radio';input.name='answer';input.value=String(i);input.required=true;label.append(input,document.createTextNode(text));choices.append(label);});
}
form.addEventListener('submit',event=>{
event.preventDefault();if(answered)return;
const selected=form.querySelector('input:checked');if(!selected)return;
answered=true;const q=questions[index],correct=Number(selected.value)===q.answer;if(correct)score++;
feedback.textContent=(correct?'Correct. ':'Try reviewing this idea. ')+q.why;
form.querySelectorAll('input').forEach(x=>x.disabled=true);submit.disabled=true;next.hidden=false;next.textContent=index===questions.length-1?'See result':'Next question';next.focus();
});
next.addEventListener('click',()=>{index++;if(index===questions.length){form.hidden=true;next.hidden=true;feedback.textContent=`Completed: ${score} / ${questions.length}`;document.querySelector('#restart').focus();}else{render();choices.querySelector('input').focus();}});
document.querySelector('#restart').addEventListener('click',()=>{index=0;score=0;render();choices.querySelector('input').focus();});render();
</script></html>
Trace and verify
Empty submission must be blocked and repeated submission cannot add points. Run all-correct, all-wrong and mixed paths; totals remain from zero to three. Restart on question two and verify clean state. Repeat using only the keyboard.
Common mistakes and boundaries
Validate empty question sets and out-of-range answers when importing data; this example uses known constants. Shuffling options without updating answer identity breaks grading, so use stable option IDs for randomization.
Practice challenge
Add review records with question ID, selected and correct option, explaining mistakes rather than showing only a cross.
Reference solution and reasoning
Append a record only on first submission, derive incorrect answers at completion, and clear records on restart. Render review as text and retain duplicate-event protection.