
:user-valid and :user-invalid: Better Form Validation UX
Form validation has always been tricky in CSS. Show errors too early and you frustrate users. Show them too late and errors go unnoticed. The :user-valid and :user-invalid pseudo-classes solve this by tracking user interaction.
The Problem with :valid and :invalid
Traditional validation pseudo-classes trigger immediately:
input:invalid {
border-color: red;
} This creates a poor user experience:
<input type="email" required />
<!-- Shows red border IMMEDIATELY, before user types anything --> The typical workaround was complex:
/* Only show validation after user has interacted */
input:invalid:not(:placeholder-shown):not(:focus) {
border-color: red;
} This is verbose, hard to remember, and doesn’t handle all cases properly.
Enter :user-valid and :user-invalid
These new pseudo-classes only trigger after the user has interacted with the field:
input:user-invalid {
border-color: red;
}
input:user-valid {
border-color: green;
} The browser determines “interaction” as:
- User has focused and blurred the field
- User has typed something
- Form has been submitted
Practical Use Cases
1. Basic Form Validation
/* No styles until user interacts */
input {
border: 2px solid #ddd;
padding: 0.75rem;
border-radius: 4px;
transition: border-color 0.2s;
}
/* Show error state after interaction */
input:user-invalid {
border-color: hsl(0 100% 50%);
background: hsl(0 100% 98%);
}
/* Show success state */
input:user-valid {
border-color: hsl(120 100% 35%);
background: hsl(120 100% 98%);
} 2. Validation Messages
.field-group {
position: relative;
margin-bottom: 1.5rem;
}
/* Hide error message by default */
.error-message {
display: none;
color: hsl(0 100% 45%);
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Show error message when field is user-invalid */
input:user-invalid + .error-message {
display: block;
}
/* Success checkmark */
input:user-valid::after {
content: "✓";
position: absolute;
right: 1rem;
color: hsl(120 100% 35%);
font-weight: bold;
} 3. Progressive Enhancement
/* Base styles work in all browsers */
input:invalid:not(:placeholder-shown):not(:focus) {
border-color: red;
}
/* Enhanced experience with :user-invalid */
@supports selector(:user-invalid) {
/* Reset fallback */
input:invalid:not(:placeholder-shown):not(:focus) {
border-color: #ddd;
}
/* Use better pseudo-class */
input:user-invalid {
border-color: red;
}
} 4. Form-Level Validation
/* Style the entire form based on validity */
form:has(:user-invalid) {
border: 2px solid hsl(0 100% 50%);
padding: 2rem;
background: hsl(0 100% 98%);
}
/* Success state */
form:has(input:user-valid):not(:has(:user-invalid)) {
border: 2px solid hsl(120 100% 35%);
background: hsl(120 100% 98%);
}
/* Disable submit button when form has errors */
form:has(:user-invalid) button[type="submit"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
} Advanced Techniques
Icon Indicators
.input-wrapper {
position: relative;
}
.input-wrapper::after {
content: "";
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
background-size: contain;
opacity: 0;
transition: opacity 0.2s;
}
/* Error icon */
input:user-invalid ~ .input-wrapper::after {
opacity: 1;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23dc3545'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E");
}
/* Success icon */
input:user-valid ~ .input-wrapper::after {
opacity: 1;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23198754'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
} Animated Validation
input {
border: 2px solid #ddd;
transition: all 0.3s ease;
}
input:user-invalid {
border-color: hsl(0 100% 50%);
animation: shake 0.3s ease;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
input:user-valid {
border-color: hsl(120 100% 35%);
animation: success-pulse 0.5s ease;
}
@keyframes success-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
} Field-Specific Messages
/* Email validation */
input[type="email"]:user-invalid {
border-color: hsl(0 100% 50%);
}
input[type="email"]:user-invalid + .hint {
display: block;
color: hsl(0 100% 45%);
}
input[type="email"]:user-invalid + .hint::before {
content: "Please enter a valid email address";
}
/* Password validation */
input[type="password"]:user-invalid + .hint::before {
content: "Password must be at least 8 characters";
}
/* Number validation */
input[type="number"]:user-invalid + .hint::before {
content: "Please enter a valid number";
} Real-World Example: Registration Form
.registration-form {
max-width: 500px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-field {
margin-bottom: 1.5rem;
}
.form-field label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #333;
}
.form-field input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 1rem;
transition: all 0.2s;
}
.form-field input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
/* Invalid state - only after user interaction */
.form-field input:user-invalid {
border-color: #dc3545;
background-color: #fff5f5;
padding-right: 3rem; /* Space for icon */
}
/* Valid state */
.form-field input:user-valid {
border-color: #28a745;
background-color: #f0fff4;
padding-right: 3rem;
}
/* Error message */
.error-message {
display: none;
margin-top: 0.5rem;
padding: 0.5rem;
background: #fff5f5;
border-left: 3px solid #dc3545;
color: #dc3545;
font-size: 0.875rem;
border-radius: 4px;
}
.form-field:has(input:user-invalid) .error-message {
display: block;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Success checkmark */
.form-field {
position: relative;
}
.form-field::after {
content: "";
position: absolute;
right: 1rem;
top: 2.75rem;
width: 24px;
height: 24px;
opacity: 0;
transition: opacity 0.2s;
}
.form-field:has(input:user-valid)::after {
content: "✓";
opacity: 1;
color: #28a745;
font-size: 1.25rem;
font-weight: bold;
}
.form-field:has(input:user-invalid)::after {
content: "✕";
opacity: 1;
color: #dc3545;
font-size: 1.25rem;
font-weight: bold;
}
/* Submit button state */
button[type="submit"] {
width: 100%;
padding: 1rem;
background: #0066cc;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button[type="submit"]:hover {
background: #0052a3;
}
/* Disable when form has invalid fields */
.registration-form:has(input:user-invalid) button[type="submit"] {
background: #ccc;
cursor: not-allowed;
opacity: 0.6;
} Browser Support
:user-valid and :user-invalid have growing support:
- Chrome/Edge: ✅ v119+ (November 2023)
- Firefox: ✅ v88+ (April 2021) - with prefix
-moz-ui-valid/-moz-ui-invalid - Safari: ❌ Not yet supported
Current global support is ~60%, requiring fallbacks for production.
Progressive Enhancement Strategy
/* Fallback for all browsers */
input:invalid:not(:focus):not(:placeholder-shown) {
border-color: red;
}
input:valid:not(:placeholder-shown) {
border-color: green;
}
/* Enhanced experience for supporting browsers */
@supports selector(:user-invalid) {
/* Reset fallback styles */
input:invalid:not(:focus):not(:placeholder-shown),
input:valid:not(:placeholder-shown) {
border-color: #ddd;
}
/* Use better pseudo-classes */
input:user-invalid {
border-color: red;
}
input:user-valid {
border-color: green;
}
}
/* Firefox with vendor prefix */
@supports selector(:-moz-ui-invalid) {
input:-moz-ui-invalid {
border-color: red;
}
input:-moz-ui-valid {
border-color: green;
}
} Comparison with Other Approaches
Old Way: JavaScript
// ❌ JavaScript required
input.addEventListener("blur", () => {
if (!input.validity.valid) {
input.classList.add("is-invalid");
}
}); Old Way: Complex CSS
/* ❌ Verbose and incomplete */
input:invalid:not(:placeholder-shown):not(:focus) {
border-color: red;
} New Way: :user-invalid
/* ✅ Simple and comprehensive */
input:user-invalid {
border-color: red;
} Accessibility Considerations
Always pair visual validation with proper ARIA attributes:
<div class="form-field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-describedby="email-error"
aria-invalid="false"
/>
<span id="email-error" class="error-message" role="alert">
Please enter a valid email address
</span>
</div> // Update aria-invalid with JavaScript
input.addEventListener("blur", () => {
input.setAttribute("aria-invalid", !input.validity.valid);
}); Common Pitfalls
1. Forgetting Required Attribute
/* Won't work without validation constraint */
input:user-invalid {
border-color: red;
} <!-- ✅ Add validation constraint -->
<input type="email" required />
<input type="text" pattern="[A-Za-z]+" />
<input type="number" min="0" max="100" /> 2. Placeholder vs Default Value
<!-- :user-invalid won't trigger on page load -->
<input type="email" placeholder="Enter email" />
<!-- But will if there's a default value that's invalid -->
<input type="email" value="invalid-email" /> 3. Form Submission
/* User-invalid doesn't trigger until user interacts OR form is submitted */
form:has(:user-invalid) {
/* This might not show errors until submit is clicked */
} Combining with Other Features
With :has()
/* Style parent based on child validity */
.form-group:has(input:user-invalid) {
background: #fff5f5;
padding: 1rem;
border-radius: 8px;
}
.form-group:has(input:user-valid) {
background: #f0fff4;
} With Container Queries
.form-container {
container-type: inline-size;
}
@container (max-width: 400px) {
/* Compact validation UI on small containers */
input:user-invalid + .error-message {
font-size: 0.75rem;
padding: 0.25rem;
}
} Conclusion
:user-valid and :user-invalid represent a significant improvement in form validation UX. They eliminate the need for complex CSS selectors and JavaScript workarounds, providing a better experience for users.
Key benefits:
- Shows validation at the right time
- Reduces user frustration
- Simplifies CSS code
- Works with native HTML validation
While browser support is still growing, progressive enhancement strategies make it safe to use today. Start implementing these pseudo-classes to create more user-friendly forms.
Browser support snapshot
Live support matrix for form-validation from
Can I Use.
Show static fallback image

Source: caniuse.com









