Exhibit 08 of 17

Keeping score

Tick things off. The score under the list is a CSS counter, and the list knows when it’s finished.

The exhibit

Before you leave the workshop

All done. The lamp thanks you.

How it works

counter-reset on the list starts two counters; every item adds one to total, and every checked item adds one to done as well. The score is written by content: counter(done) " of " counter(total) on an element after the list, so the counting has finished by the time it is printed. :not(:has(input:not(:checked))) reads “no box left unticked”.

The key CSS

Cut from this exhibit’s stylesheet as it is served.

.x-tally .checklist { counter-reset: done total; }
.x-tally .item { counter-increment: total; }
.x-tally .item:has(:checked) { counter-increment: total done; }

/* the score comes after the list, so the counters have finished counting */
.x-tally .score::after { content: counter(done) " of " counter(total) " done"; }

/* no unchecked box left: the list is finished */
.x-tally .checklist:not(:has(input:not(:checked))) .done { display: block; }

Accessibility

The checkboxes are native and fine. The score is generated content: most screen readers read it if you go to it, but nothing announces it when it changes, which a script with a live region would.

Browser support

  • CSS counters Works in this browser.Not in this browser: Universal. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.
  • :has() Works in this browser.Not in this browser: Without :has() it can’t work; the page says so instead of half-working. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.
Slop at the top