CSS-only multiple choice quizzing
I followed a link to one of those Guardian end-of-year quizzes on my phone (probably this one), and had answered a few questions before realising that it was working entirely without JavaScript (I have JavaScript disabled on my phone because a) it cuts out most of the ads, b) it cuts out lots of bandwidth and I have a limited data plan, and c) my battery lasts longer because it’s not processing tons of code to show me some text (cough, Medium)).
I found this very impressive, well done whoever worked on that, and so I thought I would take a look at how exactly they did it.
Base HTML layer
The quiz is semantic HTML, a standard form with a submit button, with each question being a set of radio inputs and labels inside a fieldset. Each label also had a tick or cross SVG (this is good, so answers aren’t purely delineated by colour). The correct answer has an extra class on its label, and some extra explanatory text. Something like this:
<form method="POST">
<fieldset>
<legend>Question?</legend>
<div class="answers">
<div class="answer">
<input type="radio" name="answers[0]" id="answer-1-1" value="1" required>
<label for="answer-1-1" class="answer__item answer__item--is-correct">
<span class="answer__icon"> <svg>...</svg> </span>
Answer 1
<span class="answer__reveal-text">Explanation</span>
</label>
</div>
<div class="answer">
<input type="radio" name="answers[0]" id="answer-1-2" value="2" required>
<label for="answer-1-2" class="answer__item">
<span class="answer__icon"> <svg>...</svg> </span>
Answer 2 </label>
</div>
<div class="answer">
<input type="radio" name="answers[0]" id="answer-1-3" value="3" required>
<label for="answer-1-3" class="answer__item">
<span class="answer__icon"> <svg>...</svg> </span>
Answer 3 </label>
</div>
<div class="answer">
<input type="radio" name="answers[0]" id="answer-1-4" value="4" required>
<label for="answer-1-4"class="answer__item">
<span class="answer__icon"> <svg>...</svg> </span>
Answer 4 </label>
</div>
</div>
</fieldset>
...
<button type="submit">Submit answers</button>
</form>
At the bottom of the form is also a results box that contains your score.
Base CSS layer
The inputs are visually hidden, so all visual interaction is with the labels (keyboard navigation of the inputs also works fine). The extra explanation texts, all answer icons, and results box are also hidden by default. The labels are grey boxes with a pointer cursor:.answer__item {
background-color: #f6f6f6;
display: block;
position: relative;
cursor: pointer;
padding: 0.75rem 1.25rem
}
The form has a is-not-results
or is-results
class
depending upon whether it’s pre/post submission, and it gives any selected
labels an extra answer__item--selected
class. This allows the CSS
to have hover/keyboard states work before submission, and to show the right
answers post submission. The full CSS source is
available on GitHub;
here’s a selection with some added comments:
/* Hover/keyboard focus should change the background colour of the item, if not yet answered */
.is-not-results :not(:checked) + .answer__item:hover, .is-not-results :not(:checked):focus + .answer__item { background-color: #dcdcdc }
/* Labels not clickable on results page */
.is-results .answer__item { cursor: default }
/* Highlight the correct answer in light green */
.is-results .answer__item--is-correct { background-color: rgba(61, 181, 64, 0.6) }
/* Highlight chosen answers in red */
.is-results .answer__item--selected { color: #ffffff; background-color: #c70000 }
/* Highlight *correctly* chosen answers in bright green */
.is-results .answer__item--is-correct.answer__item--selected { background-color: #3db540; }
/* Show extra explanatory text */
.is-results .answer__reveal-text { display: block }
/* Show the icon for the selected answer (tick or cross) */
.is-results .answer__item--selected .answer__icon { display: inline-block }
/* Do show the results box on the results page */
.is-results .message { display: block }
Marking an answer as right or wrong
So that would be enough to have a totally working quiz, letting you fill it in, submit, get the answers and your result. But this quiz gives you instant feedback, without any JavaScript. How does it do it?
First, it hides the submit button as it won’t be necessary. Then it makes
great use of the :checked
and :valid
attribute
selectors. Any question’s fieldset will only be :valid
when one of
the answers is picked, and the answer that is picked will be
:checked
. So using that, plus the adjacent sibling selector to
target the labels next to the chosen answers, it can provide equivalent entries
to each of the is-results
lines in the server-side version above:
/* Any correct answer on any answered question, highlight in light green */
:valid .answer__item--is-correct { background-color: rgba(61, 181, 64, 0.6) }
/* Show any extra explanatory text */
:valid .answer__reveal-text { display: block }
/* Any chosen answer, highlight in red */
:checked + .answer__item { color: #ffffff; background-color: #c70000 }
/* Any correctly chosen answer, highlight in bright green */
:checked + .answer__item--is-correct { background-color: #3db540; }
/* Show the icon for the selected answer */
:checked + .answer__item .answer__icon { display: inline-block }
I would think if the server-side submission filled in the form fields correctly, which it does, this CSS alone would cover the server-side results page too.
Preventing changing your answer
The quiz also stops people changing their answer once they’ve given it. This
works along the same lines as the result highlighting, by preventing pointer
events on the labels of :valid
inputs, and any user selection of
answered questions. So the act of picking an answer then disables that answer
permanently (see Pure
CSS Connect 4 for more advanced usage).
:valid { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none }
:valid + .answer__item { pointer-events: none }
Providing a score at the end
To top it all off, when you answer all the questions, you instantly get a score underneath. Again, without JavaScript.
This works by using CSS counters and the fact a form also matches
:valid
once all its inputs are valid.
So firstly, we want the message block to display when the whole form is valid:
form:valid .message { display: block }
Secondly, we need to keep score. The form can reset a counter, and then the same CSS that displays the bright green background on correct answers can increment that counter (as that will clearly also be the number of correct answers).
form { counter-reset: quiz-score }
:checked + .answer__item--is-correct { counter-increment: quiz-score }
Lastly we need to display the score. The HTML of the message box is:
<div class="message">
<div class="score-message">
You got…
<span class="score" data-question-count="14"></span>
</div>
</div>
And we can fill in the score with this bit of CSS:
.score:after { content:counter(quiz-score) "/" attr(data-question-count) }
Which prints out the CSS counter, and the contents of the attribute to give you a “9/14” type result.
Most screen readers will read generated content, but sadly not Internet Explorer 11 (Edge is okay) (sources 1 and 2), so you should think about whether that is a concern for you.
Going further (too far?)
The Guardian quiz also has a rating of your score, which only works with
JavaScript. It does this by having one div
per score, with a class
of js-atom-quiz__bucket-message--N
where N is the score, all set
to display: none
, and then JavaScript changes the correct one to
display: block
at the end.
Can we do this only with CSS? Firstly, ask ourselves, should we? It’s fine, and probably more straightforward, doing it with JavaScript, especially given our progressive base. Anyway, the answer appears to be not in general, but in Firefox, yes...
Firefox lets you create your own counter styles above and beyond the standard ones. So if you created an inline style (assuming your ratings are different per quiz) of all your quiz ratings, something like the below if it was out of 5:
@counter-style results {
system: symbolic;
symbols: "Rubbish" "Quite bad" "Mediocre" "Okay" "Good" "Excellent";
}
.score-rating-text:after {
content: counter(quiz-score, results);
}
Then the correct message would be output as generated content in that location.
Conclusion
Given how poorly some websites are written (Everyone has JavaScript, right?), it was great to find this really well-written multiple choice quiz interface. If you helped make it, let me know so I can say thanks and well done! :)