Validation Styles
Style form validation states using CSS pseudo-classes — :valid, :invalid, :required, :focus, and :placeholder-shown for real-time visual feedback.
Validation Styles
HTML5 provides built-in form validation — required, type="email", minlength, pattern, and more. But the default validation UI is minimal and inconsistent. In this final episode you will use CSS pseudo-classes to create beautiful, real-time validation feedback that guides users as they fill out a form.
HTML5 Validation Attributes
| Attribute | What It Validates | Example |
|---|---|---|
required | Field must not be empty | <input required> |
type="email" | Must be a valid email format | <input type="email"> |
minlength | Minimum character count | <input minlength="3"> |
maxlength | Maximum character count | <input maxlength="100"> |
pattern | Must match a regex pattern | <input pattern="[A-Z]{3}"> |
min / max | Numeric range | <input type="number" min="0" max="100"> |
CSS Validation Pseudo-Classes
| Pseudo-Class | When It Applies |
|---|---|
:valid | Input value passes all validation constraints |
:invalid | Input value fails one or more constraints |
:required | Input has the required attribute |
:optional | Input does not have required |
:placeholder-shown | Placeholder text is visible (input is empty) |
:focus | Input currently has keyboard focus |
:user-invalid | Input is invalid AND has been interacted with (newer browsers) |
The Problem: Premature Validation
/* This shows errors before the user even types anything! */
input:invalid {
border-color: red;
}
A required input is :invalid the moment the page loads — before the user has had a chance to type anything. Showing red borders immediately creates a poor experience. We need to show validation only after the user has interacted with the field.
Solution: Validate Only After Interaction
/* Show validation only when the input has content AND is invalid */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Show success only when the input has content AND is valid */
input:not(:placeholder-shown):valid {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
The :not(:placeholder-shown) selector ensures validation styles only appear after the user has typed something. When the placeholder is shown (input is empty), no validation colors appear. Once the user starts typing, the input shows green for valid or red for invalid in real time.
Adding Validation Icons
.validation-wrapper {
position: relative;
}
.validation-wrapper input {
padding-right: 44px;
}
/* Success icon (checkmark) */
.validation-wrapper input:not(:placeholder-shown):valid ~ .validation-icon::after {
content: "✓";
color: #10b981;
}
/* Error icon (X) */
.validation-wrapper input:not(:placeholder-shown):invalid ~ .validation-icon::after {
content: "✗";
color: #ef4444;
}
.validation-icon {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
font-weight: bold;
pointer-events: none;
}
A sibling <span class="validation-icon"></span> placed after the input displays a checkmark or X based on the validation state. The general sibling combinator (~) connects the input's state to the icon element.
Validation Messages
<div class="form-group validation-wrapper">
<label for="email">Email</label>
<input type="email" id="email" placeholder="john@example.com" required>
<span class="validation-icon"></span>
<span class="validation-msg error-msg">Please enter a valid email address</span>
<span class="validation-msg success-msg">Looks good!</span>
</div>
.validation-msg {
display: none;
font-size: 12px;
margin-top: 6px;
font-weight: 500;
}
.error-msg { color: #ef4444; }
.success-msg { color: #10b981; }
/* Show error when invalid and has content */
input:not(:placeholder-shown):invalid ~ .error-msg {
display: block;
}
/* Show success when valid and has content */
input:not(:placeholder-shown):valid ~ .success-msg {
display: block;
}
Helper text messages appear below the input based on the validation state. They are hidden by default with display: none and shown conditionally using the sibling combinator.
Focus + Validation Combined
/* Focused — neutral blue (always) */
input:focus {
border-color: #4A90D9;
box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15);
}
/* Focused + invalid + has content — red override */
input:focus:not(:placeholder-shown):invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Focused + valid + has content — green override */
input:focus:not(:placeholder-shown):valid {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
When an input is focused and empty, it shows the neutral blue border. Once the user starts typing, the border changes to reflect the validation state — green for valid, red for invalid — providing real-time feedback as they type.
Required Field Indicator
/* Add asterisk to required field labels */
.form-group:has(input:required) > label::after,
.form-group:has(select:required) > label::after {
content: " *";
color: #ef4444;
font-weight: normal;
}
The :has() selector checks if the form group contains a required input. If it does, a red asterisk is automatically appended to the label. No extra HTML needed.
Styling the Submit Button Based on Form Validity
/* Dim the submit button when form is invalid */
.styled-form:invalid button[type="submit"] {
opacity: 0.6;
cursor: not-allowed;
}
/* Full opacity when form is valid */
.styled-form:valid button[type="submit"] {
opacity: 1;
cursor: pointer;
}
The :valid and :invalid pseudo-classes also work on <form> elements. The form is :valid only when all its inputs pass validation. This lets us dim the submit button until the form is completely filled out.
Complete Validation Flow
| User Action | Input State | Visual Feedback |
|---|---|---|
| Page loads (empty input) | :placeholder-shown + :invalid | Neutral border — no validation colors |
| User clicks input | :focus + :placeholder-shown | Blue focus ring |
| User types "abc" | :focus + :invalid (for email) | Red border + error message |
| User types "abc@test.com" | :focus + :valid | Green border + success message + checkmark |
| User clicks away | :valid | Green border persists (user already interacted) |
Key Takeaways
- Never use
:invalidalone — it triggers before the user interacts, showing errors on page load - Combine
:not(:placeholder-shown)with:valid/:invalidto validate only after user input - The general sibling combinator (
~) connects input validation state to icon and message elements :has()on the form group automatically adds required field indicators:validand:invalidwork on<form>elements too — useful for styling the submit button- Combine focus state with validation for real-time feedback: blue when empty, green when valid, red when invalid