{metadata.image?.alt}

Form validation with :user-valid

This article might help you to get a better understanding of the :user-valid and :user-invalid pseudo-classes and what you can do with them.
Learn how to style HTML forms using :user-valid and :user-invalid pseudo-classes for accessible, user-friendly validation.

How to Style Forms with :user-valid and :user-invalid

If you are building a small website, you will likely need to create a form at some point.

Even a simple form, like a “Subscribe to our newsletter”, can raise questions about user experience, security, and accessibility.
By following the default behavior of form elements and using the new pseudo-classes :user-valid and :user-invalid, you can keep your forms simple and effective.

Let’s explore how it works.

What is Form Validation and why Should You Use It

Let’s start with a simple subscription form as our example.
We will ask visitors for their name and email address.

For this article, I will asume the forms are on an HTML5 page.
To understand forms better, you can do further research on form behavior and input validation. Here, we will focus only on the basics.

We need a form with two input fields—one for the name, one for the email address—and a submit button. The form should only be sent if all required information is technically valid.

For clarity, I’ve added a bit of styling to the forms on this page to position the elements vertically — this layout is almost always better for user experience, but layout is outside the scope of this article.

<form action='[the url of your form handler]'>
  <input type="text" placeholder="your name" />
  <input type="email" placeholder="your email address" />
  <input type="submit" Value="Submit" />
</form>

Which will give us a form like this (clicking submit will bring you back to this page):

Right now, no matter what a user types—or even if they leave fields empty—the form will be sent to the URL in the action attribute.
To make sure we get the correct information, we need to validate the form, checking that all required fields are filled and correct before submitting.

Security Concerns

It is important to understand that client-side form validation mainly helps the user. It can make forms easier to fill out and reduce small mistakes, but it does not guarantee form security.

Never trust user input as correct or safe. Users can bypass client-side checks, for example by disabling JavaScript or sending requests directly to your server. Always validate and sanitize all input on the server to ensure form security, protect your website from spam, and prevent malicious activity.

Using server-side validation alongside :user-valid and :user-invalid pseudo-classes helps you create forms that are both user-friendly and secure.

Attributes and Pseudo-Classes for Forms

Back to our use case: we need to collect both the name and email address. To make sure users fill in these fields, we can use the required attribute on both input elements.

Attributes provide additional information to the browser about how the HTML element should behave or be displayed.

Using the required attribute gives us two main benefits

  • Ensures the field contains some information.
  • Ensures the information matches the type of the field (for example, an email address must look like a proper email).

The required attribute performs basic checks for us and prevents the form from being submitted if these checks fail. Simple and effective!

<form action='[the url of your form handler]'>
  <input type="text" placeholder="your name" required/>
  <input type="email" placeholder="your email address" required/>
  <input type="submit" Value="Submit" />
</form>

Which will give us a form like this:

You have probably noticed that the input fields get an extra outline when they are focused.
This is an example of a pseudo-class (:focus) that I have styled across the website, making it immediately clear which element is active.

A pseudo-class is added to a CSS selector to define a specific state of the element.
One of the most common pseudo-classes is :hover, which changes the style of a link, button, or other element when the cursor is over it.

:user-(in)valid vs :(in)valid

Form inputs already support pseudo-classes for validation: :valid and :invalid.
These allow you to style input fields based on whether the entered data is correct or not.

The newer :user-valid and :user-invalid pseudo-classes do the same thing — but with one important difference: they improve the user experience.

Let’s take an input type="email" and add some simple styles to show its state depending on the validity of the input.

/* Make a thick border */
.ex_input {
  border: 2px solid black;
}

/* If valid, turn green */
#ex1:valid,
#ex2:user-valid {
  border-color: green;
}

/* If invalid, turn red */
#ex2:user-invalid,
#ex1:invalid {
  border-color: red;
}

Resulting in:

As you can see, Example 1 already shows a red border.
That’s because :invalid is applied immediately when the page loads — before the user types anything.
It turns green only after a valid email address is entered.

In Example 2, we use :user-valid and :user-invalid.
These wait until the user has interacted with the input before showing a valid or invalid style.

Both options work the same in terms of functionality — they only differ in timing and experience.
:valid reacts instantly, while :user-valid triggers after the user leaves the field (on blur).

To get the best of both worlds, you can combine them:
Use :valid and :user-valid for instant positive feedback, and skip :invalid to avoid showing an “error” state before the user even starts typing.

.ex3 {
  border: 2px solid black;
}

#ex3:valid,
#ex3:user-valid {
  border-color: green;
}

#ex3:user-invalid {
  border-color: red;
}

Resulting in:

Accessibility Concerns and Recommendations

Let’s return to our subscription form.

First, we want to tell users what each input field is for.
We can do this using the <label> element, which connects to the corresponding input via the input’s id attribute.

Labels are not only helpful for visual users, but also for people who rely on assistive technology like screen readers.
It’s therefore important to write clear, descriptive, and helpful label text.

<form action='[the url of your form handler]'>
  <label for="user_name">Your name</label>
  <input id="user_name" type="text" placeholder="your name" required />
  <label for="user_email">Your email address</label>
  <input id="user_email" type="email" placeholder="your email address" required />
  <input type="submit" Value="Submit" />
</form>

When I set the focus on the email field on my MacBook with VoiceOver enabled, it reads aloud:

“Your email address: your email address, required email.”

This information comes from several sources:

  • the label text,
  • the placeholder,
  • the required attribute, and
  • the input type.

When you submit the form, the browser automatically sets focus on any field that contains an error.
A screen reader will then read the error message, for example:

“Please include an ’@’ in the email address.”

Keep in mind that not all assistive tools work in the same way or use the same information.
That’s why it’s essential to test your forms using different accessibility tools — and if possible, ask real users of assistive technology to help test your site.

Important: As soon as you start customizing form elements or replacing them with your own components, you might lose built-in accessibility features.
In that case, you’ll need to add ARIA attributes or write custom JavaScript to make sure your forms remain usable for everyone.

Enhanced Experience

Now that we understand how form validation and pseudo-classes work, let’s see how we can use them to improve user experience — without writing a single line of JavaScript.

Required

Let’s start by giving all users clear feedback that the fields are required.
We already know that screen readers can announce this automatically, but our visually oriented users don’t get this information yet.

You could add a note above the form saying that all fields are required, but what if you have a mix of required and optional ones?
You might also add “(required)” directly in each label — but since we already marked the inputs as required in HTML, we can use that instead. It’s always best to avoid duplicate administration.

Because each input is placed directly after its label, we can use the powerful :has() pseudo-class to detect if the input is required:

label:has( + input[required]) {
  /* do what we need to do for required fields */
}

Valid or Invalid

We can use a similar approach for the :valid, :user-valid, and :user-invalid states:

label:has( + input:user-valid),
label:has( + input:valid) {
  /* add some more magic here */
}

Now let’s combine all of this.
We’ll change the border color of the input field and show a colored status message next to the label, using CSS variables to keep things flexible:

label {
  position: relative;

  /* style the ::after pseudo-element so we can
  ** easily update content, background color, and text color */
  &::after {
    content: var(--msg-content, unset);
    position: absolute;
    margin-left: auto;
    width: fit-content;
    font-size: .875rem;
    padding: 0.25rem;
    border-radius: .5rem;
    background-color: var(--msg-color, inherit);
    color: var(--msg-textcolor, inherit);
  }
}

/* let the input border react to the same variables */
input[type="text"],
input[type="email"] {
  border-color: var(--msg-color, inherit); // state color
}

/* required state */
label:has( + input[required]) {
  --msg-content: ' (required)'; 
}

/* valid state */
input:user-valid,
input:valid,
label:has( + input:user-valid),
label:has( + input:valid) {
  --msg-content: ' valid!';
  --msg-color: green;
  --msg-textcolor: white;
}

/* invalid state */
input:user-invalid,
label:has( + input:user-invalid) {
  --msg-content: ' invalid...';
  --msg-color: red;
  --msg-textcolor: white;
}

Separation of Concerns

Personally, I don’t like putting text content inside my stylesheets.
Sooner or later it will become hard to manage, especially when you’re working with a CMS.
A better approach is to define your message texts as CSS custom properties in the HTML itself:

<form action='[the url of your form handler]' style="--msg-required: ' required';--msg-valid: ' valid';---msg-invalid: ' invalid'">
label:has( + input:user-invalid) {
  --msg-content: var(--msg-invalid);
}

Note: Add a space before each message.
Remember that assistive technologies combine what they read — without the space, the label might be read as “Your namerequired” instead of “Your name required.”

All In Example

Below is a CodePen that takes it a bit further, just to show you what we are capable of with this new pseudo-classes.

Or open the PureCSS Form Validation UI example here:CodePen

Conclusion

By using only HTML and CSS, we’ve made our simple form more accessible, informative, and user-friendly — without adding a single line of JavaScript.

The key takeaway is that modern CSS gives us powerful tools to respond to user input and form states directly in the stylesheet.
By understanding how attributes and pseudo-classes work, we can create interfaces that are both functional and inclusive.

Keep in mind that with great styling power comes responsibility: always test your forms with real users and assistive technologies to make sure your improvements actually help.

23 / 11

Astro With React, Svelte, and/or Vue?

Can you use React, Svelte, Vue.js, etc, together with Astro? Let's find out!

Astro screenshot and logo
30 / 10

Why I Chose Astro for My Website

In this article, I explain why I chose Astro as my frontend framework to build a fast, lightweight, and sustainable website.

Need some help with your forms?

Forms can become quite complex with just a few requirements.
Therefore it is important that you start with sharp requirements, clean designs and good performant frontend code.

I can do that.
So. How can I help?

Just a small note — I don’t reply to spam, cold sales messages, or royal inheritance proposals. Please redirect your riches to a local good cause instead.

Success!

Your message is sent and I will respond to you as soon as possible!

I know, this message is slightly underwelming, but I will fix that soon.
In the meantime, why not browse a bit more?
Check out my articles maybe?

READ MY ARTICLES

Oh No!!

Something went wrong I'm afraid...
Your message was not sent...

Maybe try again?

Same Player Shoots Again...