Open the full-page example → · Download complete recipe (ZIP)
Try the working example
The preview runs only our supplied example code. Entries stay in the page; the practice form cannot send them. For keyboard testing, use the full-page example.
Build it step by step
- Associate each label and hint with its field. Keep error containers in the document before validation runs.
- Intercept submit before checking values. The document also blocks form submission with a content security policy.
- Use the email input validity flags and an explicit trimmed-length rule for the message. Set aria-invalid only on fields that fail.
- Focus the first invalid field and show a short overall result. Clear outdated feedback when an entry changes or the form resets.
Why it works
The browser supplies email syntax checking, while our message rule is explicit. No field has a name attribute, there is no network request, and the check button is enabled only after its handler is installed. Validation is a learning interaction, not a delivery service.
A mistake worth catching
Showing a red border alone does not explain the problem. A success message must not imply delivery when no message was sent. Client-side validation is also not a replacement for server-side validation in a real service.
Read the complete source
The ZIP also includes the local font and its licence, a README and any illustrations. The source below is the same code used in the preview.
HTML source
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Mario V.W.B.R. Obst"><meta name="robots" content="noindex, follow"><meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; form-action 'none'; object-src 'none'; base-uri 'none'">
<title>A form that explains its errors</title><link rel="stylesheet" href="style.css">
<script src="script.js" defer></script></head>
<body><main><h1>A message rehearsal.</h1><p>Use fictional details. This exercise checks your entries in this page only; nothing is sent or saved by the example.</p><form id="practice" novalidate autocomplete="off"><label for="email">Practice email address</label><input id="email" type="email" required aria-describedby="email-hint email-error"><p id="email-hint" class="hint">Try [email protected].</p><p id="email-error" class="error"></p><label for="message">Practice message</label><textarea id="message" rows="3" required minlength="10" aria-describedby="message-hint message-error"></textarea><p id="message-hint" class="hint">Write at least 10 characters after surrounding spaces are removed.</p><p id="message-error" class="error"></p><button id="check" type="submit" disabled>Check this practice message</button><button type="reset">Clear</button><p id="result" role="status"></p></form><noscript><p>JavaScript is needed for this practice check. The form cannot send data.</p></noscript></main></body></html>CSS source
@font-face{font-family:FYI;src:url("inter.ttf") format("truetype");font-weight:100 900;font-display:swap}
*{box-sizing:border-box}html{color-scheme:dark}body{margin:0;background:#20212a;color:#efedf6;font-family:FYI,sans-serif;line-height:1.6}main{max-width:960px;margin:auto;padding:24px}h1,h2{line-height:1.15}a{color:#ff9b45}button,input,select,textarea{font:inherit;color:inherit;background:#282934;border:1px solid #777582;border-radius:6px;padding:10px}button{cursor:pointer}button:disabled{cursor:default;opacity:.5}:focus-visible{outline:3px solid #ff9b45;outline-offset:4px}label{display:block;margin-top:16px}button{margin:12px 8px 12px 0}p{max-width:70ch}.hint{color:#c7c3d2} [hidden]{display:none!important}.panel{padding:20px;border:1px solid #555361;border-radius:12px;background:#282934}img{max-width:100%;height:auto}code{overflow-wrap:anywhere}input,select,textarea{max-width:100%}a,button{touch-action:manipulation}
input,textarea{width:100%}.error{color:#ffc28c}.error:empty{display:none}[aria-invalid="true"]{border:2px solid #ff9b45}JavaScript source
const form = document.querySelector('#practice');
const email = document.querySelector('#email');
const message = document.querySelector('#message');
const result = document.querySelector('#result');
function clearFeedback() {
for (const field of [email, message]) {
field.removeAttribute('aria-invalid');
document.querySelector(`#${field.id}-error`).textContent = '';
}
result.textContent = '';
}
form.addEventListener('submit', event => {
event.preventDefault();
clearFeedback();
const problems = [
[email, email.validity.valueMissing ? 'Enter a practice email address.' : email.validity.typeMismatch ? 'Use an address such as [email protected].' : ''],
[message, message.value.trim().length < 10 ? 'Write at least 10 characters, excluding surrounding spaces.' : '']
];
let firstInvalid = null;
for (const [field, error] of problems) {
if (!error) continue;
field.setAttribute('aria-invalid', 'true');
document.querySelector(`#${field.id}-error`).textContent = error;
firstInvalid ||= field;
}
if (firstInvalid) {
result.textContent = 'Please correct the highlighted fields. Nothing was sent.';
firstInvalid.focus();
} else {
result.textContent = 'The practice check passed. Nothing was sent or saved.';
}
});
form.addEventListener('reset', clearFeedback);
form.addEventListener('input', event => {
if (![email, message].includes(event.target)) return;
event.target.removeAttribute('aria-invalid');
document.querySelector(`#${event.target.id}-error`).textContent = '';
result.textContent = '';
});
document.querySelector('#check').disabled = false;
Take it one step further
Add a required subject field, its hint and its error container. Include it in validation and clearing, then test submitting with the keyboard.
Check your work
Try a narrow window, 200% zoom and keyboard-only operation. Disable JavaScript to inspect the fallback. For motion, test your reduced-motion preference. Do not use real personal details in learning forms.
Keep exploring
Read the companion guide · Practise finding small mistakes · All code recipes
Technical reference and authorship
Read the underlying platform documentation (new tab). The explanation, composition and example were written for this site; this link is a factual reference, not a source of a copied template. Inter has its own font licence in the download.
Use and adapt the original example for your own learning. For other reuse, see Legal information. About AI assistance.