LMS — Quiz Question Types
A complete set of six interactive quiz question components for an e-learning module: single-choice, multi-select, true/false, drag-to-match, fill-in-the-blank, and reorder-the-code. Each card grades independently with clear correct, incorrect, and missed states, plus a live progress bar, running score chip, and a decorative countdown timer. Keyboard-friendly throughout, calm light theme, vanilla JS only.
MCP
Code
:root {
--brand: #5b5bd6;
--brand-d: #4444c2;
--brand-50: #eeeefc;
--accent: #13b981;
--amber: #f59e0b;
--ink: #1a1a2e;
--ink-2: #44465f;
--muted: #6b6e87;
--bg: #f7f7fb;
--surface: #ffffff;
--line: rgba(26, 26, 46, 0.1);
--ok: #13b981;
--ok-50: #e6f7f0;
--danger: #e05656;
--danger-50: #fcecec;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(26, 26, 46, 0.06);
--sh-md: 0 6px 22px rgba(26, 26, 46, 0.08);
--sh-lg: 0 14px 40px rgba(68, 68, 194, 0.14);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1100px 540px at 90% -10%, #ececfb 0%, transparent 55%),
radial-gradient(900px 480px at -10% 0%, #e7f7f1 0%, transparent 50%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
padding: 28px 16px 64px;
}
.app {
max-width: 760px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 18px;
}
code {
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
font-size: 0.86em;
background: var(--brand-50);
color: var(--brand-d);
padding: 1px 6px;
border-radius: 6px;
}
/* ---------- header ---------- */
.quiz-head {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
padding: 20px 22px;
}
.quiz-head__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
}
.course { display: flex; align-items: center; gap: 14px; }
.course__badge {
width: 46px; height: 46px;
flex: none;
display: grid; place-items: center;
border-radius: 13px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
font-weight: 800;
letter-spacing: .5px;
box-shadow: var(--sh-lg);
}
.course__eyebrow {
margin: 0;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--muted);
}
.course__title { margin: 2px 0 0; font-size: 19px; font-weight: 800; }
.head-meta { display: flex; gap: 8px; align-items: center; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 600;
padding: 6px 11px;
border-radius: 999px;
}
.pill--level { background: var(--amber); color: #4a3000; background: #fff4e0; color: #8a5300; }
.pill--level .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--amber); }
.pill--time {
background: var(--brand-50); color: var(--brand-d);
font-variant-numeric: tabular-nums;
}
.progress-row {
display: flex; align-items: center; gap: 12px;
margin-top: 18px;
flex-wrap: wrap;
}
.progress {
flex: 1 1 180px;
height: 9px;
background: var(--brand-50);
border-radius: 999px;
overflow: hidden;
}
.progress__bar {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), #34d39e);
transition: width .5s cubic-bezier(.4, 0, .2, 1);
}
.progress__label { font-size: 13px; color: var(--muted); }
.progress__label strong { color: var(--ink); }
.score-chip {
margin-left: auto;
font-size: 12.5px; font-weight: 700;
background: var(--ok-50); color: #0c7a55;
padding: 5px 11px; border-radius: 999px;
}
/* ---------- question card ---------- */
.quiz { display: flex; flex-direction: column; gap: 16px; }
.q {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-sm);
padding: 22px;
transition: box-shadow .25s, border-color .25s, transform .25s;
}
.q.is-correct { border-color: rgba(19, 185, 129, .5); box-shadow: 0 8px 26px rgba(19, 185, 129, .12); }
.q.is-wrong { border-color: rgba(224, 86, 86, .45); box-shadow: 0 8px 26px rgba(224, 86, 86, .1); }
.q__head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.q__num {
font-size: 12px; font-weight: 800;
color: #fff;
background: var(--ink);
width: 28px; height: 28px;
display: grid; place-items: center;
border-radius: 9px;
}
.q__tag {
font-size: 11.5px; font-weight: 700;
text-transform: uppercase; letter-spacing: .06em;
color: var(--brand-d);
background: var(--brand-50);
padding: 4px 9px; border-radius: 999px;
}
.q__pts {
margin-left: auto;
font-size: 12px; font-weight: 600; color: var(--muted);
}
.q__prompt { margin: 0 0 16px; font-size: 16.5px; font-weight: 700; line-height: 1.4; }
.q__prompt em { font-style: normal; text-decoration: underline; text-decoration-color: var(--amber); text-underline-offset: 3px; }
/* ---------- options (single & multi) ---------- */
.opts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.opt {
display: flex; align-items: center; gap: 12px;
padding: 13px 14px;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
cursor: pointer;
background: var(--surface);
transition: border-color .18s, background .18s, transform .12s;
}
.opt:hover { border-color: var(--brand); background: #fafaff; }
.opt:active { transform: scale(.99); }
.opt:focus-visible { outline: 3px solid var(--brand-50); outline-offset: 2px; border-color: var(--brand); }
.opt__mark {
flex: none;
width: 20px; height: 20px;
border: 2px solid #c9cadb;
border-radius: 50%;
position: relative;
transition: border-color .18s, background .18s;
}
.opts--check .opt__mark { border-radius: 6px; }
.opt.is-selected .opt__mark {
border-color: var(--brand);
background: var(--brand);
}
.opt.is-selected .opt__mark::after {
content: "";
position: absolute; inset: 0;
background-image: radial-gradient(circle, #fff 0 32%, transparent 33%);
}
.opts--check .opt.is-selected .opt__mark::after {
background-image: none;
left: 5px; top: 1px; right: auto; bottom: auto;
width: 6px; height: 11px;
border: solid #fff; border-width: 0 2px 2px 0;
transform: rotate(45deg);
content: "";
}
.opt__text { font-size: 15px; }
/* graded states */
.opt.mark-correct { border-color: var(--ok); background: var(--ok-50); }
.opt.mark-correct .opt__mark { border-color: var(--ok); background: var(--ok); }
.opt.mark-wrong { border-color: var(--danger); background: var(--danger-50); }
.opt.mark-wrong .opt__mark { border-color: var(--danger); background: var(--danger); }
.opt.mark-miss { border-color: var(--ok); border-style: dashed; background: #f3fbf8; }
/* ---------- true / false ---------- */
.tf { display: flex; gap: 12px; }
.tf__btn {
flex: 1;
font-family: inherit;
font-size: 15px; font-weight: 700;
color: var(--ink-2);
padding: 15px;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
transition: border-color .18s, background .18s, transform .12s;
}
.tf__btn:hover { border-color: var(--brand); background: #fafaff; }
.tf__btn:active { transform: scale(.98); }
.tf__icon {
width: 22px; height: 22px; border-radius: 50%;
display: grid; place-items: center;
font-size: 12px;
background: var(--brand-50); color: var(--brand-d);
}
.tf__btn.is-selected { border-color: var(--brand); background: var(--brand-50); color: var(--brand-d); }
.tf__btn.mark-correct { border-color: var(--ok); background: var(--ok-50); color: #0c7a55; }
.tf__btn.mark-correct .tf__icon { background: var(--ok); color: #fff; }
.tf__btn.mark-wrong { border-color: var(--danger); background: var(--danger-50); color: #b53a3a; }
.tf__btn.mark-wrong .tf__icon { background: var(--danger); color: #fff; }
/* ---------- drag to match ---------- */
.match { display: flex; flex-direction: column; gap: 14px; }
.match__bank {
display: flex; flex-wrap: wrap; gap: 9px;
min-height: 46px;
padding: 10px;
background: #f5f5fc;
border: 1.5px dashed var(--line);
border-radius: var(--r-md);
}
.chip {
font-size: 14px; font-weight: 600;
padding: 8px 14px;
background: var(--surface);
border: 1.5px solid var(--brand);
color: var(--brand-d);
border-radius: 999px;
cursor: grab;
box-shadow: var(--sh-sm);
transition: transform .12s, box-shadow .18s;
user-select: none;
}
.chip:hover { transform: translateY(-1px); box-shadow: var(--sh-md); }
.chip:active { cursor: grabbing; }
.chip.dragging { opacity: .45; }
.chip.is-armed { box-shadow: 0 0 0 3px var(--brand-50); }
.chip:focus-visible { outline: 3px solid var(--brand-50); outline-offset: 2px; }
.match__targets { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.slot {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 11px 13px;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
transition: border-color .18s, background .18s;
}
.slot.drop-hover { border-color: var(--brand); background: var(--brand-50); }
.slot__label { font-size: 14.5px; font-weight: 500; }
.slot__drop {
flex: none;
min-width: 110px; text-align: center;
font-size: 13.5px; font-weight: 600;
color: var(--muted);
padding: 8px 12px;
border: 1.5px dashed #cdcedf;
border-radius: 999px;
}
.slot__drop.filled {
color: var(--brand-d); background: var(--surface);
border-style: solid; border-color: var(--brand);
cursor: pointer;
}
.slot.mark-correct { border-color: var(--ok); background: var(--ok-50); }
.slot.mark-correct .slot__drop.filled { border-color: var(--ok); color: #0c7a55; }
.slot.mark-wrong { border-color: var(--danger); background: var(--danger-50); }
.slot.mark-wrong .slot__drop.filled { border-color: var(--danger); color: #b53a3a; }
/* ---------- fill in the blank ---------- */
.fill {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 15px;
background: #f5f5fc;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
margin: 0;
color: var(--ink-2);
line-height: 2.2;
}
.blank {
font-family: inherit;
font-size: 14px;
padding: 5px 9px;
border: 1.5px solid var(--brand);
border-radius: 7px;
background: var(--surface);
color: var(--brand-d);
outline: none;
transition: border-color .18s, box-shadow .18s;
}
.blank:focus { box-shadow: 0 0 0 3px var(--brand-50); }
.blank.mark-correct { border-color: var(--ok); background: var(--ok-50); color: #0c7a55; }
.blank.mark-wrong { border-color: var(--danger); background: var(--danger-50); color: #b53a3a; }
.hint { margin: 12px 0 0; font-size: 13px; color: var(--muted); font-style: italic; }
/* ---------- code order ---------- */
.code-lines {
list-style: none; margin: 0; padding: 0;
display: flex; flex-direction: column; gap: 7px;
counter-reset: ln;
}
.line {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px;
background: #1e1e36;
border: 1.5px solid transparent;
border-radius: var(--r-sm);
cursor: grab;
transition: border-color .18s, transform .12s, box-shadow .18s;
}
.line code {
background: none; color: #d8d8ef; padding: 0; font-size: 13px;
white-space: pre;
}
.line__grip { color: #6b6e87; font-size: 14px; line-height: 1; cursor: grab; }
.line:hover { border-color: var(--brand); }
.line:focus-visible { outline: 3px solid var(--brand-50); outline-offset: 2px; }
.line.dragging { opacity: .5; }
.line.over { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
.line.mark-correct { border-color: var(--ok); box-shadow: inset 3px 0 0 var(--ok); }
.line.mark-wrong { border-color: var(--danger); box-shadow: inset 3px 0 0 var(--danger); }
/* ---------- buttons / feedback ---------- */
.q__foot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.btn {
font-family: inherit;
font-size: 14px; font-weight: 700;
padding: 10px 18px;
border-radius: 999px;
border: 1.5px solid transparent;
cursor: pointer;
transition: transform .12s, background .18s, box-shadow .18s, opacity .18s;
}
.btn:active { transform: scale(.97); }
.btn--check { background: var(--ink); color: #fff; }
.btn--check:hover { background: #2a2a44; }
.btn--check.is-done { background: var(--ok-50); color: #0c7a55; }
.btn--ghost { background: transparent; border-color: var(--line); color: var(--ink-2); }
.btn--ghost:hover { border-color: var(--brand); color: var(--brand-d); }
.btn--primary {
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff; padding: 13px 26px; font-size: 15px;
box-shadow: var(--sh-lg);
}
.btn--primary:hover { box-shadow: 0 18px 46px rgba(68, 68, 194, .25); }
.feedback {
margin: 0;
font-size: 13.5px; font-weight: 600;
display: none;
}
.feedback.show { display: inline-flex; align-items: center; gap: 6px; }
.feedback.ok { color: #0c7a55; }
.feedback.no { color: var(--danger); }
.feedback::before { font-weight: 800; }
.feedback.ok::before { content: "✓"; }
.feedback.no::before { content: "✕"; }
/* ---------- footer ---------- */
.quiz-foot {
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
padding: 6px 4px 0;
}
.quiz-foot__note { margin: 0; font-size: 13px; color: var(--muted); }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px;
transform: translate(-50%, 18px);
background: var(--ink); color: #fff;
font-size: 14px; font-weight: 600;
padding: 12px 20px; border-radius: 999px;
box-shadow: var(--sh-lg);
opacity: 0; pointer-events: none;
transition: opacity .28s, transform .28s;
z-index: 50;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 520px) {
body { padding: 16px 10px 48px; }
.quiz-head, .q { padding: 16px; }
.course__title { font-size: 17px; }
.q__prompt { font-size: 15.5px; }
.tf { flex-direction: column; }
.slot { flex-direction: column; align-items: stretch; }
.slot__drop { min-width: 0; }
.head-meta { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: .01ms !important; }
}(function () {
"use strict";
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
/* ---------- progress / scoring ---------- */
var graded = {}; // id -> boolean correct
var questions = Array.prototype.slice.call(document.querySelectorAll(".q"));
var total = questions.length;
document.getElementById("totalCount").textContent = String(total);
function refreshProgress() {
var ids = Object.keys(graded);
var answered = ids.length;
var correct = ids.filter(function (k) { return graded[k]; }).length;
document.getElementById("answeredCount").textContent = String(answered);
document.getElementById("progressBar").style.width =
(answered / total) * 100 + "%";
var chip = document.getElementById("scoreChip");
if (answered > 0) {
chip.hidden = false;
document.getElementById("scoreVal").textContent = String(correct);
}
}
function markGraded(q, correct, msg) {
graded[q.id] = correct;
q.classList.toggle("is-correct", correct);
q.classList.toggle("is-wrong", !correct);
var fb = q.querySelector("[data-feedback]");
if (fb) {
fb.textContent = msg;
fb.className = "feedback show " + (correct ? "ok" : "no");
}
var btn = q.querySelector("[data-check]");
if (btn) btn.classList.add("is-done");
refreshProgress();
}
/* ---------- 1 & 3: single choice / true-false (radio behaviour) ---------- */
function wireSingle(q) {
var opts = q.querySelectorAll(".opt, .tf__btn");
opts.forEach(function (opt) {
function select() {
opts.forEach(function (o) {
o.classList.remove("is-selected");
if (o.setAttribute) o.setAttribute("aria-checked", "false");
});
opt.classList.add("is-selected");
if (opt.setAttribute) opt.setAttribute("aria-checked", "true");
}
opt.addEventListener("click", select);
opt.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(); }
});
});
}
/* ---------- 2: multi-select (checkbox toggle) ---------- */
function wireMulti(q) {
q.querySelectorAll(".opt").forEach(function (opt) {
function toggle() {
var on = opt.classList.toggle("is-selected");
opt.setAttribute("aria-checked", on ? "true" : "false");
}
opt.addEventListener("click", toggle);
opt.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
});
});
}
/* ---------- 4: drag to match ---------- */
function wireMatch(q) {
var bank = q.querySelector("#bank");
var chips = q.querySelectorAll(".chip");
var slots = q.querySelectorAll(".slot");
var armed = null; // keyboard-selected chip
function placeChip(chip, dropEl) {
var slot = dropEl.closest(".slot");
// if slot already holds a chip, send it back to bank
var existing = dropEl.querySelector(".chip");
if (existing) bank.appendChild(existing);
dropEl.textContent = "";
dropEl.appendChild(chip);
dropEl.classList.add("filled");
}
chips.forEach(function (chip) {
chip.addEventListener("dragstart", function (e) {
chip.classList.add("dragging");
e.dataTransfer.setData("text/plain", chip.dataset.key);
e.dataTransfer.effectAllowed = "move";
});
chip.addEventListener("dragend", function () {
chip.classList.remove("dragging");
});
// keyboard: arm chip, then activate a slot
chip.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
chips.forEach(function (c) { c.classList.remove("is-armed"); c.setAttribute("aria-grabbed", "false"); });
if (armed === chip) { armed = null; }
else { armed = chip; chip.classList.add("is-armed"); chip.setAttribute("aria-grabbed", "true"); toast("Now pick a row to drop into"); }
}
});
});
slots.forEach(function (slot) {
var drop = slot.querySelector("[data-drop]");
slot.addEventListener("dragover", function (e) { e.preventDefault(); slot.classList.add("drop-hover"); });
slot.addEventListener("dragleave", function () { slot.classList.remove("drop-hover"); });
slot.addEventListener("drop", function (e) {
e.preventDefault();
slot.classList.remove("drop-hover");
var key = e.dataTransfer.getData("text/plain");
var chip = q.querySelector('.chip[data-key="' + key + '"]');
if (chip) placeChip(chip, drop);
});
// keyboard drop
slot.addEventListener("click", function () {
if (armed) {
placeChip(armed, drop);
armed.classList.remove("is-armed");
armed.setAttribute("aria-grabbed", "false");
armed = null;
} else {
// click a filled drop returns its chip to bank
var inSlot = drop.querySelector(".chip");
if (inSlot) { bank.appendChild(inSlot); drop.classList.remove("filled"); drop.textContent = "Drop here"; }
}
});
});
q.querySelector("[data-reset-match]").addEventListener("click", function () {
q.querySelectorAll(".slot .chip").forEach(function (c) { bank.appendChild(c); });
slots.forEach(function (s) {
var d = s.querySelector("[data-drop]");
d.classList.remove("filled");
d.textContent = "Drop here";
s.classList.remove("mark-correct", "mark-wrong");
});
});
q._gradeMatch = function () {
var answer = JSON.parse(q.dataset.answer);
var allFilled = true, correct = 0;
slots.forEach(function (slot) {
var desc = slot.dataset.desc;
var chip = slot.querySelector(".chip");
slot.classList.remove("mark-correct", "mark-wrong");
if (!chip) { allFilled = false; return; }
var ok = answer[chip.dataset.key] === desc;
slot.classList.add(ok ? "mark-correct" : "mark-wrong");
if (ok) correct++;
});
if (!allFilled) { toast("Match every row first"); return null; }
return { correct: correct === slots.length, score: correct, of: slots.length };
};
}
/* ---------- 5: fill in the blank ---------- */
function wireFill(q) {
q._gradeFill = function () {
var answers = JSON.parse(q.dataset.answer);
var inputs = q.querySelectorAll("[data-blank]");
var correct = 0;
inputs.forEach(function (inp, i) {
var val = inp.value.trim().toLowerCase().replace(/[;:]$/, "");
var ok = val === String(answers[i]).toLowerCase();
inp.classList.remove("mark-correct", "mark-wrong");
inp.classList.add(ok ? "mark-correct" : "mark-wrong");
if (ok) correct++;
});
return { correct: correct === answers.length, score: correct, of: answers.length };
};
}
/* ---------- 6: code line ordering ---------- */
function wireCode(q) {
var list = q.querySelector("#codeLines");
var dragEl = null;
function lines() { return Array.prototype.slice.call(list.querySelectorAll(".line")); }
list.querySelectorAll(".line").forEach(function (line) {
line.addEventListener("dragstart", function () { dragEl = line; line.classList.add("dragging"); });
line.addEventListener("dragend", function () {
line.classList.remove("dragging");
lines().forEach(function (l) { l.classList.remove("over"); });
});
line.addEventListener("dragover", function (e) {
e.preventDefault();
if (!dragEl || dragEl === line) return;
lines().forEach(function (l) { l.classList.remove("over"); });
line.classList.add("over");
});
line.addEventListener("drop", function (e) {
e.preventDefault();
if (!dragEl || dragEl === line) return;
var arr = lines();
var from = arr.indexOf(dragEl), to = arr.indexOf(line);
if (from < to) line.after(dragEl); else line.before(dragEl);
line.classList.remove("over");
});
// keyboard reorder
line.addEventListener("keydown", function (e) {
if (e.key === "ArrowUp" && line.previousElementSibling) {
e.preventDefault(); line.parentNode.insertBefore(line, line.previousElementSibling); line.focus();
} else if (e.key === "ArrowDown" && line.nextElementSibling) {
e.preventDefault(); line.parentNode.insertBefore(line.nextElementSibling, line); line.focus();
}
});
});
q._gradeCode = function () {
var want = q.dataset.answer.split(",");
var order = lines().map(function (l) { return l.dataset.id; });
var ok = order.join(",") === want.join(",");
lines().forEach(function (l, i) {
l.classList.remove("mark-correct", "mark-wrong");
l.classList.add(l.dataset.id === want[i] ? "mark-correct" : "mark-wrong");
});
return { correct: ok };
};
}
/* ---------- check dispatch ---------- */
function checkQuestion(q) {
var type = q.dataset.type;
var msg, res;
if (type === "single") {
var sel = q.querySelector(".opt.is-selected");
if (!sel) { toast("Pick an option first"); return; }
q.querySelectorAll(".opt").forEach(function (o) {
o.classList.remove("mark-correct", "mark-wrong");
if (o.dataset.val === q.dataset.answer) o.classList.add("mark-correct");
else if (o === sel) o.classList.add("mark-wrong");
});
var ok = sel.dataset.val === q.dataset.answer;
markGraded(q, ok, ok ? "Correct — nicely done." : "Not quite. The highlighted option is right.");
return;
}
if (type === "multi") {
var want = q.dataset.answer.split(",");
var chosen = Array.prototype.slice.call(q.querySelectorAll(".opt.is-selected"))
.map(function (o) { return o.dataset.val; });
if (chosen.length === 0) { toast("Select at least one option"); return; }
q.querySelectorAll(".opt").forEach(function (o) {
o.classList.remove("mark-correct", "mark-wrong", "mark-miss");
var inAns = want.indexOf(o.dataset.val) > -1;
var picked = o.classList.contains("is-selected");
if (inAns && picked) o.classList.add("mark-correct");
else if (!inAns && picked) o.classList.add("mark-wrong");
else if (inAns && !picked) o.classList.add("mark-miss");
});
var allRight = want.length === chosen.length &&
want.every(function (w) { return chosen.indexOf(w) > -1; });
markGraded(q, allRight, allRight ? "Perfect — all three selected." : "Some are off — dashed = missed, red = wrong.");
return;
}
if (type === "boolean") {
var b = q.querySelector(".tf__btn.is-selected");
if (!b) { toast("Choose True or False"); return; }
q.querySelectorAll(".tf__btn").forEach(function (t) {
t.classList.remove("mark-correct", "mark-wrong");
if (t.dataset.val === q.dataset.answer) t.classList.add("mark-correct");
else if (t === b) t.classList.add("mark-wrong");
});
var bok = b.dataset.val === q.dataset.answer;
markGraded(q, bok, bok ? "Correct — display:none removes it entirely." : "False is right — display:none removes the node from a11y tree and focus.");
return;
}
if (type === "match") {
res = q._gradeMatch();
if (!res) return;
markGraded(q, res.correct, res.correct ? "All matched correctly." : (res.score + " of " + res.of + " matched."));
return;
}
if (type === "fill") {
res = q._gradeFill();
markGraded(q, res.correct, res.correct ? "Exactly right." : (res.score + " of " + res.of + " blanks correct."));
return;
}
if (type === "code") {
res = q._gradeCode();
markGraded(q, res.correct, res.correct ? "Lines are in the right order." : "Order is off — green rows are in their correct slot.");
return;
}
}
/* ---------- init each question ---------- */
questions.forEach(function (q) {
var t = q.dataset.type;
if (t === "single" || t === "boolean") wireSingle(q);
if (t === "multi") wireMulti(q);
if (t === "match") wireMatch(q);
if (t === "fill") wireFill(q);
if (t === "code") wireCode(q);
var btn = q.querySelector("[data-check]");
if (btn) btn.addEventListener("click", function () { checkQuestion(q); });
});
/* ---------- submit all ---------- */
document.getElementById("submitAll").addEventListener("click", function () {
questions.forEach(function (q) {
if (!(q.id in graded)) checkQuestion(q);
});
var ids = Object.keys(graded);
var correct = ids.filter(function (k) { return graded[k]; }).length;
if (ids.length < total) {
toast("Answer all questions to submit");
} else {
toast("Score: " + correct + " / " + total + " — " + Math.round((correct / total) * 100) + "%");
}
});
/* ---------- decorative countdown timer ---------- */
var remaining = 14 * 60 + 38;
var timerEl = document.getElementById("timer");
setInterval(function () {
if (remaining <= 0) return;
remaining--;
var m = Math.floor(remaining / 60), s = remaining % 60;
timerEl.textContent = m + ":" + (s < 10 ? "0" + s : s);
}, 1000);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Quiz Question Types — Foundations of Web Craft</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="quiz-head">
<div class="quiz-head__top">
<div class="course">
<span class="course__badge" aria-hidden="true">FW</span>
<div>
<p class="course__eyebrow">Foundations of Web Craft</p>
<h1 class="course__title">Module 4 · Knowledge Check</h1>
</div>
</div>
<div class="head-meta">
<span class="pill pill--level">
<span class="dot" aria-hidden="true"></span> Intermediate
</span>
<span class="pill pill--time" id="timer" aria-live="polite">14:38</span>
</div>
</div>
<div class="progress-row">
<div class="progress" aria-hidden="true">
<div class="progress__bar" id="progressBar" style="width:0%"></div>
</div>
<span class="progress__label"><strong id="answeredCount">0</strong> of <span id="totalCount">6</span> answered</span>
<span class="score-chip" id="scoreChip" hidden>
<span id="scoreVal">0</span>/6 correct
</span>
</div>
</header>
<main class="quiz" id="quiz">
<!-- 1. Single choice -->
<article class="q" data-type="single" data-answer="b" id="q1">
<div class="q__head">
<span class="q__num">01</span>
<span class="q__tag">Single choice</span>
<span class="q__pts">2 pts</span>
</div>
<h2 class="q__prompt">Which CSS property creates a new stacking context most reliably without affecting layout flow?</h2>
<ul class="opts" role="radiogroup" aria-label="Single choice options">
<li class="opt" role="radio" tabindex="0" data-val="a">
<span class="opt__mark"></span>
<span class="opt__text"><code>float: left</code></span>
</li>
<li class="opt" role="radio" tabindex="0" data-val="b">
<span class="opt__mark"></span>
<span class="opt__text"><code>isolation: isolate</code></span>
</li>
<li class="opt" role="radio" tabindex="0" data-val="c">
<span class="opt__mark"></span>
<span class="opt__text"><code>display: block</code></span>
</li>
<li class="opt" role="radio" tabindex="0" data-val="d">
<span class="opt__mark"></span>
<span class="opt__text"><code>overflow: visible</code></span>
</li>
</ul>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
<!-- 2. Multi select -->
<article class="q" data-type="multi" data-answer="a,c,e" id="q2">
<div class="q__head">
<span class="q__num">02</span>
<span class="q__tag">Multi-select</span>
<span class="q__pts">3 pts</span>
</div>
<h2 class="q__prompt">Select <em>all</em> values that are valid <code>position</code> keywords in CSS.</h2>
<ul class="opts opts--check" aria-label="Multi-select options">
<li class="opt" role="checkbox" tabindex="0" aria-checked="false" data-val="a">
<span class="opt__mark"></span>
<span class="opt__text"><code>sticky</code></span>
</li>
<li class="opt" role="checkbox" tabindex="0" aria-checked="false" data-val="b">
<span class="opt__mark"></span>
<span class="opt__text"><code>floating</code></span>
</li>
<li class="opt" role="checkbox" tabindex="0" aria-checked="false" data-val="c">
<span class="opt__mark"></span>
<span class="opt__text"><code>fixed</code></span>
</li>
<li class="opt" role="checkbox" tabindex="0" aria-checked="false" data-val="d">
<span class="opt__mark"></span>
<span class="opt__text"><code>anchored</code></span>
</li>
<li class="opt" role="checkbox" tabindex="0" aria-checked="false" data-val="e">
<span class="opt__mark"></span>
<span class="opt__text"><code>absolute</code></span>
</li>
</ul>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
<!-- 3. True / False -->
<article class="q" data-type="boolean" data-answer="false" id="q3">
<div class="q__head">
<span class="q__num">03</span>
<span class="q__tag">True / False</span>
<span class="q__pts">1 pt</span>
</div>
<h2 class="q__prompt">An element with <code>display: none</code> is still announced by most screen readers and remains keyboard-focusable.</h2>
<div class="tf" role="radiogroup" aria-label="True or False">
<button class="tf__btn" data-val="true">
<span class="tf__icon" aria-hidden="true">✓</span> True
</button>
<button class="tf__btn" data-val="false">
<span class="tf__icon" aria-hidden="true">✕</span> False
</button>
</div>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
<!-- 4. Drag to match -->
<article class="q" data-type="match" id="q4"
data-answer='{"flex":"1D layout along a single axis","grid":"2D layout with rows and columns","z-index":"Paint order on the stacking axis"}'>
<div class="q__head">
<span class="q__num">04</span>
<span class="q__tag">Drag to match</span>
<span class="q__pts">3 pts</span>
</div>
<h2 class="q__prompt">Drag each chip to the description it best fits.</h2>
<div class="match">
<div class="match__bank" id="bank" aria-label="Draggable chips">
<div class="chip" draggable="true" data-key="grid" tabindex="0" role="button" aria-grabbed="false">Grid</div>
<div class="chip" draggable="true" data-key="z-index" tabindex="0" role="button" aria-grabbed="false">z-index</div>
<div class="chip" draggable="true" data-key="flex" tabindex="0" role="button" aria-grabbed="false">Flexbox</div>
</div>
<ul class="match__targets">
<li class="slot" data-desc="1D layout along a single axis">
<span class="slot__label">1D layout along a single axis</span>
<span class="slot__drop" data-drop>Drop here</span>
</li>
<li class="slot" data-desc="2D layout with rows and columns">
<span class="slot__label">2D layout with rows and columns</span>
<span class="slot__drop" data-drop>Drop here</span>
</li>
<li class="slot" data-desc="Paint order on the stacking axis">
<span class="slot__label">Paint order on the stacking axis</span>
<span class="slot__drop" data-drop>Drop here</span>
</li>
</ul>
</div>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<button class="btn btn--ghost" data-reset-match>Reset</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
<!-- 5. Fill in the blank -->
<article class="q" data-type="fill" id="q5"
data-answer='["box-sizing","border-box"]'>
<div class="q__head">
<span class="q__num">05</span>
<span class="q__tag">Fill in the blank</span>
<span class="q__pts">2 pts</span>
</div>
<h2 class="q__prompt">Complete the rule so padding and border are included in the declared width.</h2>
<p class="fill">
<code>*</code> { <input class="blank" data-blank type="text" aria-label="Blank 1" placeholder="property" autocomplete="off" spellcheck="false" size="10" />:
<input class="blank" data-blank type="text" aria-label="Blank 2" placeholder="value" autocomplete="off" spellcheck="false" size="9" />; }
</p>
<p class="hint">Hint: the modern CSS reset everyone copies.</p>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
<!-- 6. Code snippet (order lines) -->
<article class="q" data-type="code" id="q6"
data-answer="l1,l3,l2,l4">
<div class="q__head">
<span class="q__num">06</span>
<span class="q__tag">Order the code</span>
<span class="q__pts">3 pts</span>
</div>
<h2 class="q__prompt">Reorder the lines so the function returns a debounced version of <code>fn</code>.</h2>
<ol class="code-lines" id="codeLines" aria-label="Reorderable code lines">
<li class="line" draggable="true" data-id="l2"><span class="line__grip" aria-hidden="true">⋮⋮</span><code> return (...args) => {</code></li>
<li class="line" draggable="true" data-id="l1"><span class="line__grip" aria-hidden="true">⋮⋮</span><code>function debounce(fn, wait) {</code></li>
<li class="line" draggable="true" data-id="l4"><span class="line__grip" aria-hidden="true">⋮⋮</span><code> t = setTimeout(() => fn(...args), wait); }; }</code></li>
<li class="line" draggable="true" data-id="l3"><span class="line__grip" aria-hidden="true">⋮⋮</span><code> clearTimeout(t);</code></li>
</ol>
<p class="hint">Tip: drag rows, or focus a row and press ↑ / ↓.</p>
<div class="q__foot">
<button class="btn btn--check" data-check>Check answer</button>
<p class="feedback" data-feedback role="status"></p>
</div>
</article>
</main>
<footer class="quiz-foot">
<button class="btn btn--primary" id="submitAll">Submit quiz</button>
<p class="quiz-foot__note">You can check each question as you go.</p>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Quiz Question Types
A single knowledge-check screen from the fictional course Foundations of Web Craft, showing six distinct question patterns side by side. The header pairs a difficulty pill and live countdown with a progress bar and a running “X / 6 correct” score chip that appears as soon as the learner checks their first answer.
Every card is fully interactive and grades itself. Single-choice and true/false behave like radio groups; multi-select toggles like checkboxes and distinguishes wrong picks (red) from missed answers (dashed green). The drag-to-match card moves chips by pointer drag or by keyboard (activate a chip, then a row), and the reorder-the-code card sorts lines by drag or with the arrow keys. Fill-in-the-blank validates each input independently and tolerates trailing punctuation.
Checking an answer reveals inline feedback and color-codes the relevant options, while “Submit quiz” grades any remaining questions and toasts a final percentage. Styling is a calm light theme built entirely from CSS variables, it respects reduced-motion preferences, and the layout collapses cleanly to a mobile-first single column at ~360px. No frameworks, no build step.
Illustrative UI only — fictional courses, not a real learning platform.