Episode 6 of 6

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

AttributeWhat It ValidatesExample
requiredField must not be empty<input required>
type="email"Must be a valid email format<input type="email">
minlengthMinimum character count<input minlength="3">
maxlengthMaximum character count<input maxlength="100">
patternMust match a regex pattern<input pattern="[A-Z]{3}">
min / maxNumeric range<input type="number" min="0" max="100">

CSS Validation Pseudo-Classes

Pseudo-ClassWhen It Applies
:validInput value passes all validation constraints
:invalidInput value fails one or more constraints
:requiredInput has the required attribute
:optionalInput does not have required
:placeholder-shownPlaceholder text is visible (input is empty)
:focusInput currently has keyboard focus
:user-invalidInput 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 ActionInput StateVisual Feedback
Page loads (empty input):placeholder-shown + :invalidNeutral border — no validation colors
User clicks input:focus + :placeholder-shownBlue focus ring
User types "abc":focus + :invalid (for email)Red border + error message
User types "abc@test.com":focus + :validGreen border + success message + checkmark
User clicks away:validGreen border persists (user already interacted)

Key Takeaways

  • Never use :invalid alone — it triggers before the user interacts, showing errors on page load
  • Combine :not(:placeholder-shown) with :valid/:invalid to 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
  • :valid and :invalid work 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