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
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-invalidWorks 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 countersWorks in this browser.Not in this browser: Universal. Checked 2026-10-14: Chromium 149 yes, Firefox 141 yes.
