Browsers can validate form data before it’s sent to the server — without a single line of JavaScript. Built-in validation speeds up feedback for the user, but it never replaces server-side checks.
The required attribute
Prevents a form from being submitted with an empty field:
<input type="text" name="username" required />
<textarea name="message" required></textarea>
<select name="category" required>
<option value="">-- Choose --</option>
<option value="python">Python</option>
</select>
Trying to submit with an empty required field shows a browser tooltip.
Length and range constraints
<!-- Text length -->
<input
type="text"
name="title"
minlength="5"
maxlength="100"
required
/>
<!-- Number range -->
<input
type="number"
name="age"
min="18"
max="120"
step="1"
required
/>
<!-- Date range -->
<input
type="date"
name="publish_date"
min="2024-01-01"
max="2030-12-31"
/>
| Attribute | Applies to | Description |
|---|---|---|
minlength |
text, email, password, textarea |
Minimum number of characters |
maxlength |
same | Maximum number of characters |
min |
number, date, time |
Minimum value |
max |
same | Maximum value |
step |
number, date |
Step between allowed values |
Input type as a validator
Some <input> types automatically validate the format:
<!-- Checks for @ and a domain dot -->
<input type="email" name="email" required />
<!-- Checks URL format -->
<input type="url" name="website" placeholder="https://example.com" />
<!-- Phone — format varies by browser, not reliable alone -->
<input type="tel" name="phone" />
Pattern matching
A regular expression for precise format validation:
<!-- Only Latin letters and digits, 3–20 characters -->
<input
type="text"
name="username"
pattern="[a-zA-Z0-9]{3,20}"
title="Latin letters and digits only, 3 to 20 characters"
required
/>
<!-- Phone number format -->
<input
type="tel"
name="phone"
pattern="\+1\d{10}"
placeholder="+12025550100"
title="Format: +1XXXXXXXXXX"
/>
<!-- URL-friendly post slug -->
<input
type="text"
name="slug"
pattern="[a-z0-9-]+"
title="Lowercase letters, digits, and hyphens only"
/>
The title attribute holds the hint text the browser shows on error.
The :valid and :invalid pseudo-classes
CSS can style fields based on their validation state:
/* Default state — neutral border */
input {
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 0.5rem;
outline: none;
transition: border-color 0.2s;
}
/* Field in focus */
input:focus {
border-color: #3b82f6;
}
/* Valid field (after user interaction) */
input:valid {
border-color: #22c55e;
}
/* Invalid field */
input:invalid {
border-color: #ef4444;
}
Problem: :invalid fires immediately on page load for all required fields, before the user has typed anything. The fix is to use :user-invalid (modern browsers) or only activate styles after the first submit attempt:
/* Fires only after user interaction — supported in Chrome 119+ */
input:user-invalid {
border-color: #ef4444;
}
Or add a class to the form on submit via JavaScript:
<style>
.was-validated input:invalid {
border-color: #ef4444;
}
</style>
<form id="contact-form">
<input type="email" name="email" required />
<button type="submit">Send</button>
</form>
<script>
document.getElementById("contact-form").addEventListener("submit", (e) => {
e.currentTarget.classList.add("was-validated");
});
</script>
Disabling built-in validation
Sometimes you need fully custom validation logic. The novalidate attribute on the form disables browser validation:
<form action="/contact/" method="post" novalidate>
<!-- Validation handled by JavaScript or Django -->
</form>
Where responsibility lies
Built-in HTML validation solves the UX problem: fast feedback before a request is sent. But it’s trivially bypassed — via DevTools, curl, or any HTTP client.
Server-side validation is always required:
# Django validates form fields automatically
class ContactForm(forms.Form):
name = forms.CharField(min_length=2, max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, min_length=10)
def contact(request):
form = ContactForm(request.POST or None)
if form.is_valid():
# data is clean — process it
pass
return render(request, "contact.html", {"form": form})
Rule: HTML validation is for user convenience. Server-side validation is for data security. Neither alone is sufficient.
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!