Form Validation

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.

: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); }

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; }

: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; }

: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; }

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; }

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; }

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

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".

Key Takeaways