Both forms below are identical HTML. The left uses :invalid (what most tutorials show). The right uses :user-invalid (the fix). Notice the left form already shows red borders before you've typed anything.
Each field uses a different constraint. Use :user-invalid styling. Try valid and invalid values and observe the browser behavior.
Type in the field below. The ValidityState grid updates in real time to show which properties are true. The active (red) pill shows what's triggering the invalid state.
:has() + :user-invalidThe error and success messages below appear and disappear using pure CSS — no JavaScript event listeners. The label color also changes. Watch the log for zero JS events.
novalidate + per-error ValidityState messages + :user-invalid styling + :has() for CSS errors + password match + formnovalidate draft button.