0 Pure CSS7 with JSPublished 2026-10-05Free · no sign-up
Seven validation patterns that help people fix mistakes: errors that wait until you have typed, messages linked to their fields, an error summary at the top, a live password checklist, a tick for valid input, a rule for checkbox groups and a gentle shake for a wrong code.
:invalid matches an empty required field the moment the page loads, which paints the form red before anyone types. :user-invalid only matches after the person has changed or left the field, so this form stays calm until there is something to correct.
View code
<form class="fv1" novalidate><label for="fv1e">Work email</label><input id="fv1e" type="email" required placeholder="name@company.com" aria-describedby="fv1m"><p class="e" id="fv1m">Enter an email address like name@company.com.</p><button>Continue</button></form>
var f = document.querySelector('.fv1');
f.addEventListener('submit', function (e) { e.preventDefault(); var i = f.querySelector('input'); if (!i.checkValidity()) { i.focus(); i.setAttribute('aria-invalid', 'true'); } else { i.removeAttribute('aria-invalid'); } });
02. Inline Messages with aria-describedby
ValidationAccessibility
Each error message has an id and is linked to its field with aria-describedby, and the field gets aria-invalid, so a screen reader reads the label, the message and the value together. Messages appear on blur and update as the person types.
function msg(i) {
var v = i.validity;
if (v.valueMissing) return 'This field is required.';
if (v.tooShort) return 'Use at least ' + i.minLength + ' characters.';
if (v.rangeUnderflow) return 'You must be ' + i.min + ' or older.';
if (v.rangeOverflow || v.badInput) return 'Enter a real age.';
return '';
}
document.querySelectorAll('.fv2 input').forEach(function (i) {
var e = document.getElementById(i.getAttribute('aria-describedby'));
function check() { var m = msg(i); e.textContent = m; i.setAttribute('aria-invalid', m ? 'true' : 'false'); }
i.addEventListener('blur', check);
i.addEventListener('input', function () { if (i.getAttribute('aria-invalid') === 'true') check(); });
});
03. Error Summary at the Top
ValidationForms
On submit, a summary box lists every problem as a link to its field and receives focus, the pattern used by many government services. It helps people who cannot see all the inline errors, and each link jumps straight to the field to fix.
var f = document.querySelector('.fv3'), sum = f.querySelector('.sum'), ul = sum.querySelector('ul');
var text = { fv3n: 'Enter your full name', fv3p: 'Enter a postcode, like SW1A 1AA' };
f.addEventListener('submit', function (e) {
e.preventDefault(); ul.innerHTML = ''; var bad = [];
f.querySelectorAll('input').forEach(function (i) { var ok = i.checkValidity(); i.setAttribute('aria-invalid', !ok); if (!ok) bad.push(i); });
if (!bad.length) { sum.hidden = true; return; }
bad.forEach(function (i) { var li = document.createElement('li'), a = document.createElement('a'); a.href = '#' + i.id; a.textContent = text[i.id]; a.onclick = function (ev) { ev.preventDefault(); i.focus(); }; li.appendChild(a); ul.appendChild(li); });
sum.querySelector('h3').textContent = bad.length === 1 ? 'There is 1 problem' : 'There are ' + bad.length + ' problems';
sum.hidden = false; sum.focus();
});
04. Password Rules Checklist
ValidationPassword
The rules are listed under the password field and each one ticks as it is met. Every rule's state is written in text (met or not met) in a visually hidden span, so the checklist is not colour-only. The field also has a show and hide button.
View code
<div class="fv4"><label for="fv4p">New password</label><div class="w"><input id="fv4p" type="password" autocomplete="new-password" aria-describedby="fv4r"><button type="button" aria-pressed="false" aria-label="Show password">Show</button></div><ul id="fv4r"><li data-r="len">At least 10 characters<span class="sr"> not met</span></li><li data-r="num">Contains a number<span class="sr"> not met</span></li><li data-r="case">Upper and lower case letters<span class="sr"> not met</span></li></ul></div>
var i = document.getElementById('fv4p'), lis = document.querySelectorAll('.fv4 li'), b = document.querySelector('.fv4 .w button');
var tests = { len: function (v) { return v.length >= 10; }, num: function (v) { return /\d/.test(v); }, case: function (v) { return /[a-z]/.test(v) && /[A-Z]/.test(v); } };
i.addEventListener('input', function () { lis.forEach(function (li) { var ok = tests[li.dataset.r](i.value); li.classList.toggle('ok', ok); li.querySelector('.sr').textContent = ok ? ' met' : ' not met'; }); });
b.onclick = function () { var show = i.type === 'password'; i.type = show ? 'text' : 'password'; b.textContent = show ? 'Hide' : 'Show'; b.setAttribute('aria-pressed', show); };
05. Valid State with a Tick
ValidationFeedback
Positive feedback for fields where people often doubt themselves, such as a shop address that must follow rules. A tick appears inside the field when it is valid, using :user-valid, and a short text confirmation replaces the hint for screen readers.
View code
<div class="fv5"><label for="fv5u">Choose a shop address</label><div class="w"><span aria-hidden="true">shop.app/</span><input id="fv5u" required pattern="[a-z0-9\-]{3,20}" aria-describedby="fv5m"></div><p id="fv5m">3-20 lowercase letters, numbers or hyphens.</p></div>
var i = document.getElementById('fv5u'), p = document.getElementById('fv5m'), hint = p.textContent;
i.addEventListener('blur', function () { p.textContent = i.checkValidity() ? 'shop.app/' + i.value + ' is a valid address.' : hint; i.setAttribute('aria-invalid', !i.checkValidity()); });
06. Checkbox Group Validation
ValidationCheckbox
HTML cannot require at least one checkbox in a group, so this form checks it on submit and puts the error on the fieldset: the message is linked with aria-describedby and focus goes to the first checkbox. The same idea works for any group rule.
var f = document.querySelector('.fv6'), fs = f.querySelector('fieldset'), e = f.querySelector('.e');
f.addEventListener('submit', function (ev) {
ev.preventDefault();
var any = f.querySelector('input:checked');
fs.classList.toggle('bad', !any);
e.textContent = any ? '' : 'Choose at least one topic.';
if (!any) f.querySelector('input').focus();
});
f.addEventListener('change', function () { if (f.querySelector('input:checked')) { fs.classList.remove('bad'); e.textContent = ''; } });
07. Gentle Shake on Error
ValidationAnimation
A short horizontal shake draws attention to a wrong code, with the error also stated in text and announced by a live region. The shake is a 300ms keyframe animation that is skipped under reduced motion, where the red border and message are enough.
var f = document.querySelector('.fv7'), i = f.querySelector('input'), p = f.querySelector('p');
f.addEventListener('submit', function (e) {
e.preventDefault(); i.classList.remove('bad'); void i.offsetWidth;
if (i.value === '123456') { p.style.color = '#86efac'; p.textContent = 'Code accepted.'; i.removeAttribute('aria-invalid'); return; }
i.classList.add('bad'); i.setAttribute('aria-invalid', 'true'); p.style.color = ''; p.textContent = 'That code did not match. Try 123456 in this demo.'; i.focus();
});
About these CSS Form Validation
Wait before showing errors
:invalid matches empty required fields as soon as the page loads. :user-invalid and :user-valid wait until the person has edited or left the field, so the form does not start covered in red.
Connect messages to fields
Give each message an id and point to it from the input with aria-describedby, and set aria-invalid="true" on the field. A screen reader then reads the label, the error and the value together.
Summarise long forms
On submit, list every problem in a box at the top, with each item linking to its field, and move focus to the box. Inline messages stay too, for people looking at the field.
Say what to do, not just what is wrong
Enter an email like name@company.com is more useful than Invalid input. Never rely on red alone: add text, an icon or a thicker border.
Frequently asked questions
Do I still need server-side validation?
Yes. Browser validation is for convenience; always validate again on the server.
Which browsers support :user-invalid?
All current major browsers. Older ones ignore the rule, and the script fallback still sets aria-invalid.
Why novalidate on the forms?
It turns off the browser's own bubbles so the custom messages are the only ones shown.