1. Web Design
  2. HTML/CSS
  3. HTML

Input Element Pseudo-Classes to Improve User Experience

Scroll to top
8 min read

Forms are an integral part of many websites. They allow us to take user input and then act on the provided data. Some examples of form submission include filling out your name and address while making a purchase. Similarly, you might have filled out registration forms to create an account on a website.

Forms on websites generally require users to enter a variety of data. This can include numbers and strings with specific formatting requirements. Check out our tutorial on form input validation using HTML5 and regex.

In this tutorial, we will learn about different pseudo-classes that we can integrate in our forms to make them more user-friendly.

Indicating Whether Input Is Required or Optional

People generally don't like filling out long forms. While some of your users might fill out all the form fields, most of them will likely fill out the bare minimum. This means that it is in everyone's best interest if you keep your forms short. Even if you plan on using some additional fields, make sure you don't force users to fill them out.

Keeping some fields optional will give users the option to skip providing that piece of information if they are unwilling, while still completing the task at hand.

It's also a good practice to let users know beforehand which fields are required and which are optional. One way of doing this easily is with the help of the :required and :optional pseudo-classes.

Let's implement a form that marks fields as required or optional. Our form markup would look like this:

1
<form>
2
  <div class="input-wrapper">
3
    <label for="fname">Name</label>
4
    <input type="text" name="fname" id="fname">
5
    <span></span>
6
  </div>
7
  <div class="input-wrapper">
8
    <label for="uname">Username</label>
9
    <input type="text" name="uname" id="uname" required>
10
    <span></span>
11
  </div>
12
  <div class="input-wrapper">
13
    <label for="email">Email Address</label>
14
    <input type="email" name="email" id="email" required placeholder="hello@me.com">>
15
    <span></span>
16
  </div>
17
  <div class="input-wrapper">
18
    <label for="password">Password</label>
19
    <input type="text" name="password" id="password">
20
    <span></span>
21
  </div>
22
  <button>Submit</button>
23
</form>

There are two approaches we could take here. The first approach would have involved us marking the required fields as such in the markup itself. The second uses pseudo-elements that we will attach to the span tag added after each input element.

We need an extra span tag because ::before and ::after don't work on input elements. The span tag also needs to be after the input elements because right now there are no selectors to target elements that come before the current element. However, we will be able to use the adjacent sibling selector to target the span elements. The following CSS will add a label for both optional and required input elements appropriately:

1
input {
2
  border-radius: 5px;
3
  border: 2px solid black;
4
  padding: 1rem;
5
}
6
7
input + span {
8
  position: relative;
9
}
10
11
input + span::before {
12
  font-size: 60%;
13
  font-weight: bold;
14
  color: white;
15
  padding: 0.5rem;
16
  font-weight: bold;
17
  width: 80px;
18
  position: absolute;
19
  left: 0.5rem;
20
  text-align: center;
21
  top: -1rem;
22
  border-radius: 5px;
23
}
24
25
input:required + span::before {
26
  content: "Required";
27
  background: #3949AB;
28
}
29
30
input:not(:required) + span::before {
31
  content: "Optional";
32
  background: #2196F3;
33
}

The span element was positioned relatively so that we can use absolute positioning on the ::before pseudo-element with respect to the span element. We use the :not selector in combination with :required in order to filter out the required elements. You can see the results in the following CodePen demo:

Indicating Whether Input Is Valid or Invalid

In our other tutorial, we learned how to validate form input. Now, we will see how we can visually change form elements to indicate whether the input is valid or invalid. Two pseudo-classes that we use for this purpose are :valid and :invalid.

The :valid selector will match any input or form element whose content has been validated successfully. Let's say that we have an input field that accepts email addresses; we could make the input green when it's deemed valid. Similarly, we could also limit the length of usernames to be between 4 to 12 characters and so on.

The :invalid selector will match any input or form element whose content could not be validated successfully. For example, let's say you want the username to be at least 4 characters long, but a user enters only 3 characters. In this case, the input field will match the :invalid selector.

One more thing to keep in mind is that a required input will stay invalid as long as it is empty. On the other hand, optional inputs will stay valid even if you leave them empty.

We will expand upon the previous example by using the same markup and only adding the following CSS to add labels about valid and invalid inputs.

1
input + span::before, input + span::after {
2
  font-size: 60%;
3
  font-weight: bold;
4
  color: white;
5
  padding: 0.25rem 0.5rem;
6
  font-weight: bold;
7
  width: 80px;
8
  position: absolute;
9
  left: 0.5rem;
10
  text-align: center;
11
  top: -0.8rem;
12
  border-radius: 5px;
13
}
14
15
input:valid + span::after {
16
  content: "Valid";
17
  background: #388E3C;
18
  top: 1.2rem;
19
}
20
21
input:invalid + span::after {
22
  content: "Invalid";
23
  background: #C62828;
24
  top: 1.2rem;
25
}

Setting the value of the top property to 1.2rem allows us to position the pseudo-element slightly lower than it would naturally be. Most other CSS properties are shared between the ::before and ::after pseudo-elements. The following CodePen demo shows the result:

As you can see, the name and email fields are valid by default because they are optional. However, try filling out the email field with something invalid like the word "potato" and you will see the label content change from valid to invalid.

At this point, the form feels very crowded. I have added labels for every possibility to show you how to implement them yourself. We will now make some changes to the CSS to remove unnecessary labels.

We can get rid of the Optional text while keeping the required fields labeled as Required. It would be implied that other fields are optional. Another change that we will make is the removal of the :valid pseudo-class selector to mark fields as valid.

The label marking fields as Invalid also seems unnecessary for now because the users haven't even interacted with the form elements. An alternative selector that we can use in place of :invalid is the :user-invalid selector, which only invalidates an element after the user has interacted with it.

Browser support is currently very limited for :user-invalid. However, it is expected to rise in future.

Here is the CSS that accomplishes all this:

1
input:required + span::before {
2
  content: "Required";
3
  background: black;
4
}
5
6
input:user-invalid {
7
  border: 2px solid red;
8
}
9
10
input:user-invalid + span::after {
11
  content: "Invalid";
12
  background: #C62828;
13
  top: 1.2rem;
14
}
15
16
input:not(:required):user-invalid + span::after {
17
  top: 0.2rem;
18
}

The last selector applies to optional elements which might become invalid after user interaction. We use this selector to place the label in a slightly higher position.

We also modify the markup a little bit with the minlength and maxlength attributes. Here is the final markup:

1
<form>
2
  <div class="input-wrapper">
3
    <label for="fname">Name</label>
4
    <input type="text" name="fname" id="fname" minlength="3" maxlength="10">
5
    <span></span>
6
  </div>
7
  <div class="input-wrapper">
8
    <label for="uname">Username</label>
9
    <input type="text" name="uname" id="uname" minlength="4" maxlength="10" pattern="[a-zA-Z0-9]+" required>
10
    <span></span>
11
  </div>
12
  <div class="input-wrapper">
13
    <label for="email">Email Address</label>
14
    <input type="email" name="email" id="email" placeholder="hello@me.com">
15
    <span></span>
16
  </div>
17
  <div class="input-wrapper">
18
    <label for="password">Password</label>
19
    <input type="password" name="password" id="password" minlength="8" required>
20
    <span></span>
21
  </div>
22
  <button>Submit</button>
23
</form>

There are many more attributes that you can use for input validation. Placing appropriate regex inside the pattern attributes makes the validation even more powerful by helping you specify rules for usernames, passwords, email addresses, etc. In our example, we will stick to the basics.

Open the demo in Firefox to see the :user-invalid class in action.

The name in the input field now needs to be at least 3 characters long. Try entering only two characters before moving to the next field, and you will see the Invalid label. Similarly, try entering an invalid email address into the email input field, and you will see that it turns invalid as soon as the email field loses focus.

Styling Disabled and Enabled Form Elements

Sometimes, we have to develop forms where some form elements have to be disabled based on user input. This could be either because filling out those values is no longer necessary or because they have been prefilled with some values based on other data.

You can target disabled elements in your forms using the :disabled pseudo-class. You cannot select or click on disabled elements to provide any input. They also stop taking focus. Input elements can be disabled by using the disabled attribute.

Form input elements are enabled by default, and you can target them using the :enabled selector. Enabled form elements can be selected and take focus. You can also provide text or other input to them.

In this section, I will show you a simple example where we will disable two input elements, and their values will be dictated by other input elements. We will use the following CSS to indicate that the elements are disabled:

1
input:disabled {
2
  border: 2px solid gray;
3
  color: gray;
4
  background: #EEE;
5
}

Here is the JavaScript that tracks any change in the quantity input value and updates the price per unit automatically.

1
const quantityElem = document.querySelector("input#quantity");
2
const priceElem = document.querySelector('input#price');
3
4
quantityElem.addEventListener('change', (event) => {
5
  
6
  let quantity = event.target.value;
7
  let price = 200;
8
  if(quantity >= 10) {
9
    let deduction = Math.floor(quantity/5)*5;
10
    price -= deduction;
11
  }
12
  
13
  priceElem.value = price;
14
});

The following CodePen shows this in action. You won't be able to focus on disabled elements or change their values, but they will be updated automatically.

Final Thoughts

We should always try to make the form filling process as simple for users as possible. Using these selectors will help us provide visual cues to users about required or optional inputs, valid or invalid inputs, and disabled or enabled inputs.

You should also try to make your requirements for filling out forms very clear to users. For example, if usernames need to use a specific set of characters or stay within a character length limit, you should specify that beforehand. Read this tutorial about form validation to learn how we can show helpful messages to users in case of errors.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.