Score history
Two independent scoreboards. Each shows the current score, the previous score, the last change (signed), the direction (up or down) and a short visual bump or dip on every change. +1, −1 and Reset.
Three questions with three options each. The first answer counts. After answering, the quiz shows whether it was right, marks the correct option, shows an explanation and enables Next. At the end it shows the score and a message, and "Try again" starts a fresh session. Question data and the answer key are static HTML (class="correct").
| name | value | previous | changes |
|---|---|---|---|
| Waiting for the preview to start. | |||
Several scalar states working together (current card, three answers, score) without arrays or objects. The first answer counts because answered options are natively disabled (0.5.3), derived from the quiz’s state.
:has(), ::before text and counters.display: none for the other cards.A fully native quiz is possible with radio buttons, :has(:checked) and CSS counters (counters can even count checked correct answers). It cannot lock the first answer, though, and paging needs radio-button tricks.
Locking the first answer, the score, paging with a single Next button, and restarting.
disabled in the same task as the answer, so a second click is never dispatched (tested: three clicks in one task, real double click). The 0.5.1 conditional on: rules are gone.content with alternative text.--a1…--a3) because “Try again” can only reset states on its own ancestors. With attr:, options derive aria-pressed and disabled from those quiz states, so restart clears them too (tested). What still cannot be expressed: a reset of states owned by the options themselves, and returning focus to the first question.focus:). Answering disables the focused option, and focus moves to Next. Next and “Try again” move focus to the new card’s heading (tabindex="-1"). There is no focus trap: Tab and Shift+Tab work normally.disabled Next would drop focus after every question, so Next stays focusable and shows a nudge (“Choose an answer first”) instead.disabled and the chosen one has aria-pressed="true" (0.5.3). aria-pressed marks the choice, not a toggle; a radio group would be the stricter pattern./* ---- state: all scalar, owned by the quiz ---------------------------------------------------- --q: current card (1–3 questions, 4 = result). --aN: the option chosen for question N (its position, 0 = not answered yet). --score: correct first answers. */.quiz { --q: state(1); --score: state(0); --a1: state(0); --a2: state(0); --a3: state(0); --nudge: state(false); }/* One card at a time. Hidden cards are display: none (not focusable, not announced). */.quiz > .cards > .card { display: none; }.quiz > .cards > .card:nth-var(--q) { display: block; }/* ---- answering: the first answer counts ------------------------------------------------------ attr: (0.5.3): once a question is answered, its options are natively disabled, so a second click (even in the same task) is never dispatched and cannot score again. The chosen option reports aria-pressed="true", the others "false". */.q1 .opt { on: click(set(--a1, sibling-index()), set(--nudge, false)); attr: disabled when(--a1 > 0); }.q2 .opt { on: click(set(--a2, sibling-index()), set(--nudge, false)); attr: disabled when(--a2 > 0); }.q3 .opt { on: click(set(--a3, sibling-index()), set(--nudge, false)); attr: disabled when(--a3 > 0); }.q1 .opt.correct { on: click(set(--a1, sibling-index()), increment(--score), set(--nudge, false)); }.q2 .opt.correct { on: click(set(--a2, sibling-index()), increment(--score), set(--nudge, false)); }.q3 .opt.correct { on: click(set(--a3, sibling-index()), increment(--score), set(--nudge, false)); }.question .opt { attr: aria-pressed "false"; }.q1 .opt:nth-var(--a1), .q2 .opt:nth-var(--a2), .q3 .opt:nth-var(--a3) { attr: aria-pressed "true"; }.question .opt:disabled { cursor: default; color: inherit; }/* ---- feedback: the chosen option, the correct one, the verdict and the explanation ---------- */.q1 .opt:nth-var(--a1), .q2 .opt:nth-var(--a2), .q3 .opt:nth-var(--a3) { border-color: #b42318; background: #fef3f2; }.quiz:when(--a1 > 0) .q1 .opt.correct,.quiz:when(--a2 > 0) .q2 .opt.correct,.quiz:when(--a3 > 0) .q3 .opt.correct { border-color: #166534; background: #dcfce7; }.quiz:when(--a1 > 0) .q1 .opt.correct::after,.quiz:when(--a2 > 0) .q2 .opt.correct::after,.quiz:when(--a3 > 0) .q3 .opt.correct::after { content: " ✓" / " (correct answer)"; }.question .explain, .question .verdict { display: none; }.quiz:when(--a1 > 0) .q1 :is(.explain, .verdict),.quiz:when(--a2 > 0) .q2 :is(.explain, .verdict),.quiz:when(--a3 > 0) .q3 :is(.explain, .verdict) { display: block; }.question .verdict { font-weight: 600; color: #b42318; }/* Real DOM text (text:, 0.5.1): the verdict is chosen by the cascade. */.question .verdict { text: "Not quite."; }/* The chosen option is the correct one: native :has() over the :nth-var() marker. */.q1:has(.opt.correct:nth-var(--a1)) .verdict,.q2:has(.opt.correct:nth-var(--a2)) .verdict,.q3:has(.opt.correct:nth-var(--a3)) .verdict { color: #166534; text: "Correct!"; }/* ---- navigation: one persistent Next button (keeps keyboard focus) ------------------------- */.quiz > .nav > .next { on: click(set(--nudge, true)); }.quiz:when((--q == 1 and --a1 > 0) or (--q == 2 and --a2 > 0) or (--q == 3 and --a3 > 0)) > .nav > .next { on: click(increment(--q), set(--nudge, false));}.quiz:when(--q == 4) > .nav { display: none; }.quiz > .nav > .nudge { display: none; margin: 0; color: #b42318; }.quiz:when(--nudge) > .nav > .nudge { display: block; }/* ---- focus (focus:, 0.6.0) --------------------------------------------------------------------- Answering disables the focused option, so focus moves to Next (the next step); Next and "Try again" move it to the new card's heading (tabindex="-1" in the HTML). */.quiz > .nav > .next { focus: false; }.quiz:when((--q == 1 and --a1 > 0) or (--q == 2 and --a2 > 0) or (--q == 3 and --a3 > 0)) > .nav > .next { focus: true; }.quiz > .cards > .card > h2 { focus: false; }.quiz > .cards > .card:nth-var(--q) > h2 { focus: true; }.quiz > .cards > .card > h2:focus:not(:focus-visible) { outline: none; }/* ---- score and result: real DOM text (text:, 0.5.1) --------------------------------------- */.quiz > .scoreline { color: #555; margin: 0 0 8px; }.quiz .sc { text: var(--score); }.quiz .qn { text: var(--q); }.quiz:when(--q == 4) > .scoreline > .progress-text { display: none; }.result .final { font-size: 22px; font-weight: 700; }.result .praise { text: "Review the explanations and try again."; }.quiz:when(--score == 3) .result .praise { text: "Perfect score!"; }.quiz > .cards > .result > .restart { on: click(set(--q, 1), set(--score, 0), set(--a1, 0), set(--a2, 0), set(--a3, 0), set(--nudge, false)); }Source: the library's examples/interactive-quiz/. style.css from its first section comment; the layout above it runs but is not shown. Written in the 0.8 syntax (on:, bind:, text:, attr:), which 0.9.0 still runs; see Migrating from 0.8 for the 0.9 forms.
Two independent scoreboards. Each shows the current score, the previous score, the last change (signed), the direction (up or down) and a short visual bump or dip on every change. +1, −1 and Reset.
Two independent tab sets. Each tab reports a real aria-selected, inactive panels are hidden, one tab is disabled in the HTML, and the "Billing" tab is disabled until a checkbox in the first panel is checked.
A pressed-state toggle button and a disclosure button. Their aria-pressed and aria-expanded attributes are real DOM attributes, kept in sync with Reactive CSS state.