Exhibit 07 of 17

A form that tells you off, politely

Fill the form in badly. Fields complain only after you’ve touched them, and a line counts what’s left to fix.

The exhibit

Commission something small and impossible. Required fields say so.

As you would like it on the tin. The Slopwright needs something to call you.
Nothing is sent. It is only checked for an @ and a dot. That doesn’t look like an email address.
He works in dozens at most. Between 1 and 12, please.
Pick something with a lid.

Received, in the sense that nothing was sent anywhere. This form has nowhere to go.

How it works

:invalid matches an empty required field the moment the page loads, which is rude. :user-invalid waits until you have changed the field and left it, or tried to send the form. .field:has(:user-invalid) then shows that field’s own message. Every complaining field increments a counter, and the summary line prints it.

The rules themselves (required, an email shape, 1 to 12) are HTML attributes, checked by the browser. Sending goes to #sent on this page: nothing leaves it.

The key CSS

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

/* :user-invalid waits until you've interacted; :invalid would shout at an empty form */
.x-validation :is(input, select):user-invalid { border-color: var(--no); border-width: 2.5px; background: color-mix(in srgb, var(--no) 8%, var(--paper)); }
.x-validation .field:has(:user-invalid) .err { display: block; }
.x-validation .field:has(:user-invalid) .hint { display: none; }
.x-validation .field:has(:user-valid) label::after { content: " ✓"; color: var(--yes); }

/* count what's wrong with counters, and say it once something is */
.x-validation .field:has(:user-invalid) { counter-increment: bad; }
.x-validation .order:has(:user-invalid) .summary::after {
  content: counter(bad) " to fix before it can go.";
  color: var(--no);
}

Accessibility

Partly worse than a script. The messages appear visually but aren’t announced, and they aren’t tied to their fields with aria-describedby (pointing at a hidden message would read it out even when the field is fine). The browser’s own validation does announce the first problem when you try to send. A script could set aria-invalid, link each message and move focus to the first error.

Browser support

  • :user-invalid Works in this browser.Not in this browser: Errors show only when you send the form, from the browser’s own validation. 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.
  • CSS counters Works in this browser.Not in this browser: Universal. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.
Slop at the top