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.
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.
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.
.ex_input {
border: 2px solid black;
}
#ex1:valid,
#ex2:user-valid {
border-color: green;
}
#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]) {
}
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) {
}
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;
&::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);
}
}
input[type="text"],
input[type="email"] {
border-color: var(--msg-color, inherit); // state color
}
label:has( + input[required]) {
--msg-content: ' (required)';
}
input:user-valid,
input:valid,
label:has( + input:user-valid),
label:has( + input:valid) {
--msg-content: ' valid!';
--msg-color: green;
--msg-textcolor: white;
}
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.