Bismillâh, kardeşim. Checkout forms are where a lot of trust breaks. A user types their name, address, maybe a phone number, and hits Submit, expecting the site to tell them clearly if something is wrong before calling the server. The cheapest path to a careful checkout is the one browsers already ship: HTML5 validation. No React Hook Form, no Zod, no library. Just the attributes you already know.
The attributes that do the work
Every <input> and <textarea>
can carry validation rules right in the markup:
<input type="text" name="name" required minlength="2" autocomplete="name">
<input type="email" name="email" required autocomplete="email">
<input type="tel" name="phone" pattern="^[+0-9\s\-()]{7,20}$">
<textarea name="addr" required minlength="10" maxlength="300" rows="3"></textarea>
<input type="checkbox" name="terms" required>
requiredblocks submit if empty.type="email"requires "a@b.c" shape.minlength/maxlengthenforce length bounds.patternaccepts a regex; everything must match.autocompleteis the single most skipped attribute in checkout. It gives the browser permission to fill fields from the user's saved profile. A 2-second checkout instead of a 40-second one. Always add it.
The :user-invalid pseudo-class
CSS has a selector that activates only when the user has touched the field and left it in an invalid state:
input:user-invalid, textarea:user-invalid {
border-color: #f87171;
box-shadow: 0 0 0 4px rgba(248, 113, 113, 0.15);
}
Compared with :invalid, which triggers the moment
the page loads (an empty required field is technically invalid),
:user-invalid waits until the user interacts. The
difference is enormous: no red borders on a form nobody has
touched yet.
The JavaScript layer
HTML5 validation blocks the submit if any field is invalid, but browsers show a single generic tooltip that reads badly. A small script can reveal a friendlier message per-field:
const ERRORS = {
valueMissing: 'Required.',
typeMismatch: 'Looks wrong, check the format.',
tooShort: 'A bit longer please.',
patternMismatch: 'Unexpected characters.'
}
function messageFor(input) {
const v = input.validity
if (v.valueMissing) return ERRORS.valueMissing
if (v.typeMismatch) return ERRORS.typeMismatch
if (v.tooShort) return ERRORS.tooShort
if (v.patternMismatch) return ERRORS.patternMismatch
return ''
}
form.querySelectorAll('input, textarea').forEach(el => {
el.addEventListener('blur', () => updateError(el))
el.addEventListener('input', () => updateError(el))
})
The ValidityState object on every input tells you
exactly which rule failed. Eight boolean flags:
valueMissing: required but emptytypeMismatch: wrong shape for type (email, url, etc.)patternMismatch: doesn't match the regextooShort/tooLong: below/above length boundsrangeUnderflow/rangeOverflow: below/above numeric boundsstepMismatch: not on the allowed stepbadInput: unparseable inputcustomError: you set it viasetCustomValidity
The submit handler
form.addEventListener('submit', e => {
e.preventDefault()
let allValid = true
form.querySelectorAll('input, textarea').forEach(el => {
updateError(el)
if (!el.validity.valid) allValid = false
})
if (!allValid) {
const first = form.querySelector(':invalid')
first && first.focus()
return
}
// submit to backend
})
Two details most submit handlers miss:
-
Focus the first invalid field. The user
shouldn't have to scroll up looking for the problem.
form.querySelector(':invalid')returns it for free. - Show errors for all invalid fields before the first failure, not just one at a time. Users hate fixing one problem only to discover there are two more.
The novalidate attribute
When you add novalidate to the form, the browser
won't show its native bubble popup. Your JavaScript is in
charge of all messaging. Always use it when you want custom
errors; never use it when you're shipping the form without
JavaScript (the browser bubble is better than nothing).
Honest defaults
Every field in the demo has a plain-language placeholder and an
aria-label when the visible label wouldn't be read.
Don't hide the label inside the placeholder alone; placeholders
disappear when the field has text, and screen readers sometimes
skip them. Always pair a visible label with the input.
On asking only what you need
A checkout form is a request the user is filling out for you. Every extra field is a small emanet weighing on them. Ask for: name, email, shipping address, terms. Anything else (phone, tax ID, second email, preferred delivery time) should be optional or wait until after the first order. The Qur'an talks about mizan (Mutaffifîn 83:1–3): give full measure, don't demand more than you return. The same ethic applies to forms: ask only what you'll use.
Tricks worth carrying
-
autocomplete tokens matter. Use the standard
ones:
name,email,tel,street-address,postal-code,country. The browser (and 1Password, LastPass, etc.) auto-fills from the user's profile. -
inputmodeon top oftype:numericfor postal code,telfor phone. Better mobile keyboards. -
Never use a regex that rejects valid emails.
Real-world emails include
+, long TLDs, apostrophes.type="email"does RFC-ish validation for you. Don't addpatternto it. -
Labels on the checkbox too. A checkbox without
a label is unreachable by assistive tech. Wrap the checkbox
inside the
<label>and all is well.
Takeaways
- HTML5 validation does 80% of checkout validation for free.
:user-invalidis the CSS pseudo-class you've been waiting for.- Read
input.validityto know exactly which rule failed. - Focus the first invalid field. Don't make the user hunt.
- Ask only what you need. Every optional field honors the user's time.
Next post: "Honest pricing: Mutaffifîn 83, full measure in e-commerce."
Validate kindly. Redirect the form to good, kardeşim.