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! :)