What You'll Learn
Use :valid and :invalid pseudo-classes for validation styling
Style required and optional fields with :required and :optional
Validate number ranges with :in-range and :out-of-range
Show validation only after user interaction with :placeholder-shown
Implement pattern matching validation using the pattern attribute
Create custom error and success messages with CSS sibling selectors
Understand CSS validation limitations and when JavaScript is needed
Introduction to CSS Form Validation
CSS provides powerful pseudo-classes for styling form validation states without JavaScript. These pseudo-classes respond to HTML5 validation attributes like required, min, max, pattern, and input types like email and url. CSS validation is purely visual and doesn't replace server-side validation.
The key challenge with CSS validation is timing. You want to show validation feedback after users interact with fields, not immediately on page load. Using :placeholder-shown and :not() selectors allows you to delay validation styling until users start typing.
CSS Validation is Visual Only:
CSS validation styling provides user feedback but doesn't prevent form submission or validate data. Always implement server-side validation and consider using JavaScript for client-side validation logic. CSS enhances the user experience but isn't a security measure.
:valid and :invalid States
The :valid and :invalid pseudo-classes match inputs based on their validation state. An input is valid when it satisfies all constraints from attributes like required, type, pattern, min, and max.
<input type="email" required placeholder="user@example.com">
input:valid {
border-color: #10b981;
}
input:invalid {
border-color: #ef4444;
}
input:focus:invalid {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
Problem with Immediate Validation:
The :invalid state applies immediately on page load for required fields, which creates a poor user experience. Users see error states before they've had a chance to enter data. See the next section for a better approach.
Delayed Validation with :placeholder-shown
Use :placeholder-shown to show validation only after users start typing. This pseudo-class matches inputs when their placeholder is still visible, meaning the field is empty. Combine it with :not() to target fields with content.
/* No validation styling while placeholder is shown (field empty) */
input:placeholder-shown {
border-color: #d1d5db;
}
/* Show valid state after user starts typing */
input:not(:placeholder-shown):valid {
border-color: #10b981;
}
/* Show invalid state after user starts typing */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
}
Best Practice:
Always use :not(:placeholder-shown) to delay validation feedback. This creates a better user experience by showing errors only after users have interacted with the field. Remember to include a placeholder (even a space) for this to work.
:required and :optional
The :required pseudo-class matches inputs with the required attribute. The :optional pseudo-class matches inputs without required. Use these to visually distinguish mandatory fields.
input:required {
border-left: 4px solid #f59e0b;
}
input:optional {
border-left: 4px solid #6b7280;
}
input:required:valid {
border-left-color: #10b981;
}
/* Add asterisk to required labels */
label.required:after {
content: " *";
color: #ef4444;
}
Semantic HTML:
The required attribute is both a validation constraint and semantic information for screen readers. Always use it instead of relying solely on visual indicators like asterisks.
:in-range and :out-of-range
For number, date, and time inputs with min and max attributes, use :in-range and :out-of-range to style values within or outside the valid range.
<input type="number" min="18" max="100" placeholder="Enter age">
input[type="number"]:in-range {
border-color: #10b981;
background-color: #f0fdf4;
}
input[type="number"]:out-of-range {
border-color: #ef4444;
background-color: #fef2f2;
}
Real-Time Feedback:
Range validation provides immediate feedback as users type, helping them stay within acceptable values without submitting the form. This is particularly useful for age, quantity, and date range inputs.
Pattern Matching with Error Messages
The pattern attribute accepts regular expressions for custom validation. Combine it with CSS sibling selectors to show contextual error messages.
<label for="phone">Phone Number</label>
<input type="tel" id="phone"
placeholder="555-123-4567"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
required>
<div class="error-message">Format: 555-123-4567</div>
<div class="success-message">Valid phone number!</div>
.error-message,
.success-message {
display: none;
margin-top: 6px;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.9em;
}
/* Show error when invalid after typing */
input:not(:placeholder-shown):invalid ~ .error-message {
display: block;
background: #fef2f2;
color: #ef4444;
border-left: 3px solid #ef4444;
}
/* Show success when valid */
input:not(:placeholder-shown):valid ~ .success-message {
display: block;
background: #f0fdf4;
color: #10b981;
border-left: 3px solid #10b981;
}
Pattern Attribute:
The pattern attribute uses JavaScript regular expressions. Common patterns include phone numbers, postal codes, usernames, and custom ID formats. Always provide a clear error message explaining the expected format.
Visual Icons for Validation States
Add checkmark and error icons using background images or pseudo-elements to provide quick visual feedback about validation status.
/* Valid state with checkmark icon */
input:not(:placeholder-shown):valid {
border-color: #10b981;
padding-right: 40px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%2310b981' stroke-width='2' d='M5 10l3 3 7-7'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
/* Invalid state with X icon */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
padding-right: 40px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23ef4444' stroke-width='2' d='M5 5l10 10M15 5L5 15'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
SVG Data URIs:
Inline SVG icons using data URIs keep your CSS self-contained. Encode the SVG with proper URL encoding (%23 for # in colors). This technique works for simple icons without external dependencies.
Complete Form Example
A practical form combining multiple validation techniques: delayed feedback, custom error messages, visual icons, and accessible markup.
<form>
<div class="form-group">
<label for="name" class="required">Full Name</label>
<input type="text" id="name"
placeholder="John Doe"
minlength="2"
required>
<div class="error-message">
Please enter your full name (at least 2 characters)
</div>
<div class="success-message">Looks good!</div>
</div>
<div class="form-group">
<label for="email" class="required">Email</label>
<input type="email" id="email"
placeholder="john@example.com"
required>
<div class="error-message">
Please enter a valid email address
</div>
<div class="success-message">Valid email!</div>
</div>
<div class="form-group">
<label for="phone" class="required">Phone</label>
<input type="tel" id="phone"
placeholder="555-123-4567"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
required>
<div class="hint">Format: 555-123-4567</div>
<div class="error-message">
Please use the format: 555-123-4567
</div>
<div class="success-message">Valid phone number!</div>
</div>
<button type="submit">Submit Form</button>
</form>
.form-group {
margin-bottom: 1.5rem;
position: relative;
}
label.required:after {
content: " *";
color: #ef4444;
}
input {
width: 100%;
padding: 10px 12px;
border: 2px solid #d1d5db;
border-radius: 4px;
font-size: 16px;
transition: all 0.2s;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
/* Valid state */
input:not(:placeholder-shown):valid {
border-color: #10b981;
padding-right: 40px;
background-image: url("checkmark.svg");
background-repeat: no-repeat;
background-position: right 12px center;
}
/* Invalid state */
input:not(:placeholder-shown):invalid {
border-color: #ef4444;
padding-right: 40px;
background-image: url("error-x.svg");
background-repeat: no-repeat;
background-position: right 12px center;
}
/* Error messages */
.error-message,
.success-message {
display: none;
margin-top: 6px;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.9em;
}
input:not(:placeholder-shown):valid ~ .success-message {
display: block;
background: #f0fdf4;
color: #10b981;
border-left: 3px solid #10b981;
}
input:not(:placeholder-shown):invalid ~ .error-message {
display: block;
background: #fef2f2;
color: #ef4444;
border-left: 3px solid #ef4444;
}
/* Field hints */
.hint {
font-size: 0.85em;
color: #6b7280;
margin-top: 4px;
}
Browser Support and Limitations
CSS validation pseudo-classes have excellent browser support, but there are limitations to be aware of when implementing form validation.
Browser Support
:valid and :invalid - Excellent support in all modern browsers
:required and :optional - Excellent support in all modern browsers
:in-range and :out-of-range - Good support, works only with number/date/time inputs
:placeholder-shown - Good support in modern browsers (IE 11 not supported)
CSS Validation Limitations
CSS validation is purely visual and has several limitations:
Cannot prevent form submission
Cannot show custom browser validation tooltips
Cannot validate across multiple fields (e.g., password confirmation)
Cannot perform asynchronous validation (e.g., checking username availability)
Cannot calculate or transform values
Limited error message customization compared to JavaScript
Security Reminder:
CSS and client-side JavaScript validation are for user experience only. Always validate and sanitize input on the server. Malicious users can bypass client-side validation entirely by crafting HTTP requests directly.
Accessibility Considerations
Accessible form validation requires more than visual styling. Follow these practices to ensure all users can understand validation states.
ARIA Attributes
While CSS handles visual feedback, add ARIA attributes for screen reader users:
<label for="email">Email</label>
<input type="email" id="email"
aria-required="true"
aria-invalid="false"
aria-describedby="email-error">
<div id="email-error" class="error-message" role="alert">
Please enter a valid email address
</div>
Color Contrast
Ensure validation colors meet WCAG contrast requirements. Don't rely on color alone to convey validation state. Use icons, borders, and text messages together.
Error Announcements
CSS cannot trigger screen reader announcements. For dynamic error messages that appear after typing, use JavaScript to update aria-invalid and ensure errors are announced with role="alert".
Progressive Enhancement:
Start with semantic HTML and native validation, add CSS for visual feedback, then enhance with JavaScript for dynamic ARIA updates and complex validation logic. This ensures the form works at every level.
Key Takeaways
Validation Pseudo-classes: Use :valid, :invalid, :required, :optional, :in-range, :out-of-range
Delayed Feedback: Use :not(:placeholder-shown) to show validation only after users start typing
Pattern Matching: Use pattern attribute with regular expressions for custom validation formats
Sibling Selectors: Use ~ to show/hide error messages based on input state
Visual Icons: Add SVG icons using background images for quick visual validation feedback
Required Indicators: Mark required fields with asterisks using label:after pseudo-elements
Range Validation: :in-range and :out-of-range only work with number, date, and time inputs
CSS is Visual Only: CSS validation doesn't prevent submission or validate data, only provides user feedback
Server Validation Required: Always validate on the server, CSS is for UX enhancement only
Accessibility Matters: Combine CSS styling with ARIA attributes and semantic HTML for full accessibility