How to visually display an invalid input in CSS after submitting a form

- Andrés Cruz - ES En español

How to visually display an invalid input in CSS after submitting a form

One of the most common problems when working with HTML5 validation is that required fields visually appear as invalid from the first initial page render, even before the user has interacted with the form. In real-world forms, this behavior is not only confusing: it creates a poor user experience that can increase form abandonment.

In this article, I explain why it happens, how :invalid actually works in HTML5, and, above all, how to display visual errors on inputs only after submitting, using modern CSS and, if necessary, a lightweight JavaScript fallback.

What actually happens when an input is :invalid in HTML5?

Form validation in HTML5 operates through the constraint validation system. The browser evaluates each field according to its attributes (required, type, pattern, min, max, etc.) and determines whether the value meets the defined constraints. If it does not, the field immediately enters an invalid state.

When :invalid is triggered and why it appears on page load

A key point that often surprises anyone encountering this for the first time:

An empty <input required> is already invalid from the moment the page loads, even if the user hasn't touched anything.

Therefore, applying this CSS rule:

input:invalid {
  border: 2px solid red;
}

…causes the fields to appear in red even before the user types a single letter. In my case, this was the first real problem I encountered: the validation was working correctly, but the timing was terrible.

The browser does not distinguish whether the user "hasn't started yet" or "has already made a mistake." To the browser, the field simply does not satisfy the rules of the required attribute.

Visual validation of required fields with CSS

With HTML5, we can use the required attribute to tell the browser that a field is mandatory. From there, we can leverage the presence of that attribute directly in CSS to provide visual feedback to the user, without needing JavaScript.

<form>
  <input required />
  <input type="submit" />
</form>

If the field is empty and the user attempts to submit the form, the browser will automatically cancel the submission and display its native validation message:

Form with HTML5 validation error message

Try submitting the empty form and you will see the browser's error message.

If we want to visually customize fields that have the required attribute —for example, showing a yellow warning border— we can use the :required pseudo-class in CSS:

/* Required fields: yellow warning border */
form input:required {
  border: 2px solid yellow;
}

Difference between :valid and :invalid

We can go a step further and combine both pseudo-classes to provide differentiated feedback depending on the state of the field:

input:valid {
  border: 2px solid green;
}
input:focus:invalid {
  border: 2px solid red;
}

An important idea emerges here: conditioning :invalid to the field's focus using :focus. This way, the field is only marked in red while the user is actively interacting with it, preventing it from appearing in red on page load.

However, there was still a problem: this approach does not cover the case of a failed submit attempt.

The UX problem: visible errors before interaction

Although :focus:invalid prevents showing errors on page load, it does not completely cover the most critical scenario:

  • The user doesn't touch any field.
  • They click directly on "Submit".
  • The browser blocks the submission.
  • But visually… nothing happens on the form.

From the user's perspective, the form "isn't working." Here I realized that the issue was not the validation itself, but when to show the visual feedback.

Why marking empty inputs as invalid is a bad experience

Validation errors should only appear when:

  • The user attempts to submit the form, or
  • they have already interacted with a field and left it with an incorrect value.

Never before. Displaying red borders on empty inputs conveys a constant sense of error, even when the user hasn't done anything wrong yet. It is like receiving an error message on a form before typing a single character.

Complete example with :focus:invalid

Using pseudo-classes we can do really interesting things: assign a green color to the input when the value is valid and red when it is not, but only while the user is editing that field (with active focus):

/* Valid email field: green */
form input[type="email"]:required:valid {
  border: 2px solid green;
}
/* Invalid email field while focused: red */
form input[type="email"]:focus:required:invalid {
  border: 2px solid red;
}

The reason for combining :focus before :invalid is that, upon loading the page, the field is empty and therefore already has a default :invalid state. Adding :focus ensures that this red state is only visible when the user is inside the field.

Complete form with visual validation in CSS

A more comprehensive example, combining required and optional fields:

/* Required fields: yellow warning */
form input:required {
  border: 2px solid yellow;
}
/* Field with valid value: green */
form input:valid {
  border: 2px solid green;
}
/* Field with invalid value while focused: red */
form input:focus:invalid {
  border: 2px solid red;
}
<form>
  <input required type="text"  name="nombre"  placeholder="First name" />
  <input          type="text"  name="nombre2" placeholder="Middle name" />
  <input required type="email" name="email"   placeholder="Your email address" />
  <input required type="tel"   name="tel"     placeholder="Your phone number" />
  <input          type="url"   name="url"     placeholder="Your website" />
</form>

Mandatory fields (first name, email, and phone) will show a yellow border at rest, green if the value is valid, and red only while the user is typing an incorrect value. Optional fields (middle name and website) will only show green if they contain a valid value.

Using :user-invalid (the correct solution)

The :user-invalid pseudo-class was designed specifically to solve this issue. Unlike :invalid, which evaluates from the moment the page loads, :user-invalid is only triggered when:

  • The user has interacted with the field and left it invalid, or
  • they have attempted to submit the form at least once.
input:user-invalid {
  border: 2px solid red;
}

This avoids the classic problem of red inputs on page load. When I discovered this pseudo-class, it was the turning point: the behavior aligned exactly with what any user expects.

:user-invalid is the best way to display validation errors only after submission using pure CSS, without a single line of JavaScript.

It is a modern pseudo-class with good compatibility across modern browsers (Chrome 119+, Firefox 88+, Safari 16.5+), but lacks support in very old versions. For those cases, there is the fallback described below.

Compatibility and alternatives when :user-invalid is not available

If you need complete compatibility with older browsers, or simply want more control over when the error state is activated, there is a very simple and effective pattern based on JavaScript and a CSS class.

Fallback with JavaScript and the .submitted class

JavaScript:

document.querySelector("form").addEventListener("submit", function () {
  this.classList.add("submitted");
});

CSS:

/* By default: neutral style on invalid inputs */
input:invalid {
  border: 1px solid #ccc;
}
/* Only after submit: highlight errors in red */
form.submitted input:invalid {
  border: 2px solid red;
}

This approach is highly robust and predictable:

  • Before submit: fields have neutral styles, with no error borders.
  • After submit: invalid fields are highlighted in red.

In real-world projects where I need to ensure broad compatibility, I continue to rely on this pattern because it is clear, easy to maintain, and does not depend on experimental pseudo-classes.

Best practices for visual form validation

  • When to use CSS only
    • Simple forms with few fields.
    • Basic validation using required, type="email", or pattern.
    • Projects targeting modern browsers.
    • Clear UX that does not require custom error messages.
  • When you need JavaScript
    • Custom error messages per field.
    • Complex or cross-field validations.
    • Compatibility with legacy browsers.
    • Conditional logic between fields (for example, "if A is checked, B is required").
  • The correct order of layers:
    • HTML first: defines the structure and constraints with native attributes.
    • CSS for visual feedback: :valid, :invalid, :user-invalid.
    • JavaScript strictly as progressive enhancement: for custom messages or compatibility.

Frequently asked questions about form validation with CSS

  • Why does :invalid trigger on page load?
    • Because an empty required field already violates validation rules according to the HTML5 specification. The browser does not wait for the user to interact.
  • What is the difference between :invalid and :user-invalid?
    • :invalid depends solely on whether the value meets the field's rules.
    • :user-invalid also depends on user interaction or a submit attempt.
  • Can visual validation be done without JavaScript?
    • Yes, completely, by using CSS pseudo-classes such as :valid, :invalid, :user-invalid, and combinations with :focus. JavaScript is only needed as a fallback for compatibility or to customize error messages.
  • Is it recommended to hide errors until submit?
    • Yes. In my experience, it significantly improves form perception and reduces user frustration, especially on long forms or those with complex validations.
  • What happens if the user corrects the error after submitting?
    • With the .submitted class approach, the error will remain visible until the field becomes valid (at which point input:invalid stops applying). With :user-invalid, the browser handles this automatically.

Conclusion

The real challenge of form validation lies not in the validation logic itself, but in when to display the error.

Using :invalid without context often leads to a frustrating experience. Instead:

  • :focus:invalid improves real-time feedback as the user types.
  • :user-invalid provides ideal behavior: errors are visible only after submit or interaction.
  • A small JavaScript fallback with the .submitted class guarantees full compatibility.

If you control the timing of visual feedback, your forms won't just work better: they'll feel better too.

Learn how to perform validations on forms using HTML only, without JavaScript.

Discover how to control :invalid in CSS and show errors only after form submission using HTML5, :user-invalid and UX best practices.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.