Pseudo- classes in CSS: Styling Form Fields Based on Input

Let’s take a look at some pseudo-classes that are specific to form fields and form field input. These pseudo-classes can be used to style fields based on the validity of user input, whether the field is required or currently enabled.

All of the pseudo-classes that follow are specific to forms. As a result, there’s less of a need to limit the scope with a selector. Using :enabLed won’t introduce side effects for <span> elements. Limiting the scope is helpful, however, when you want to style various types of form controls differently.

:enabled and :disabled

As their name suggests, these pseudo-classes match elements that have (or lack) the HTML5 disabled attribute. This can be elements such as <input> , <seLect> , <button> or <fieLdset> :

<button type=”submit” disabled>Save draft</button>

Form elements are enabled by default. That is, they only become disabled if the disabled attribute is set. Using input:enabLed will match every input element that doesn’t have a disabled attribute. Conversely, button:disabLed will match all button elements with a disabled attribute:

button:disabled {

opacity: .5;


The image below shows the :enabLed and :disabLed states for our <button> element.

:required and :optional

Required and optional states are determined by the presence or absence of the required attribute on the field. For example:


<label for=”email”>Email:</label>

<input type=”email” id=”email” name=”email” placeholder=

→”example:” required>


Most browsers only indicate whether a field is required once the form is submitted. With the :required pseudo-class, we can indicate to the user that the field is required before submission. For example, the following CSS will add a yellow border to our email field:

input:required {

border: 1px solid #ffc107;


The :optionaL class works similarly, by matching elements that don’t have a required attribute. For example, take the following CSS:

select:optional {

border: 1px solid #ccc;


This produces the following result in Firefox 86.


Unlike the other form-related pseudo-classes we’ve covered, -.checked only applies to radio and checkbox form controls. As the name indicates, this pseudo-class lets us define separate styles for selected inputs.

In order to create custom radio button and checkbox inputs that work well across browsers, we’ll need to be a little bit clever with our selectors. Let’s combine an adjacent sibling combinator, a pseudo-element, and :checked to create custom radio button and checkbox controls. For example, to change the style of a label when its associated radio button is checked, we could use the following CSS:

[type=radio]:checked + label {

font-weight: bold;

font-size: 1.1rem;


This makes the label bold and increases its size when its associated control is checked. We can improve this, though, by using the ::before pseudo-element with our <LabeL> element to inject a custom control:

[type=radio] {


appearance: none removes default browser styles for radio buttons and other elements.

Safari supports this property with a -webkit- prefix.


-webkit-appearance: none;

appearance: none;


[type=radio] + label::before {

background: #fff; content: ”;

display: inline-block;

border: 1px solid #444;

border-radius: 1000px;

height: 1.2rem;

margin-right: 1em;

vertical-align: middle;

width: 1.2rem;


[type=radio]:checked + label::before {

background: #4caf50;


This gives us the customized controls you see below.

In order for this technique to work, of course, our HTML needs to be structured appropriately:

  • The <LabeL> element must be immediately adjacent to its <input>
  • The form control must have an id attribute in addition to the name attribute (for example, <input type=”radio” id=”chocoLate” name=”fLavor”> ).
  • The label must have a for attribute, and its value must match the ID of the form control (for example, <LabeL for=”chocoLate”>ChocoLate</LabeL> ).

Associating the <LabeL> using for with the input ensures that the form input will be selected when the user clicks or taps the label or its child pseudo-element ( ::before ).


The :indeterminate pseudo-class lets you set styles for elements that are in an

indeterminate state. Only three types of elements can have an indeterminate state:

  • <progress> elements, when it’s unclear how much work remains (such as when waiting for a server response)
  • grouped input[type=radio] form controls, before the user selects an option
  • input[type=checkbox] controls, when the indeterminate attribute is set to true (which can only be done via DOM scripting)

Let’s look at an example using the <progress> element:


<label for=”upload”>Uploading progress</label>

<progress max=”100″ id=”upload” aria-describedby=,,progress-text,,></progress>

<span id=”progress-text”>0 of <i>unknown</i> bytes.</span>


Notice here that we haven’t included a value attribute. For most WebKit- and Blink-based browsers, the presence or absence of the value attribute determines whether a <progress> element has an indeterminate state. Firefox, on the other hand, sets an indeterminate state for <progress> elements when the value attribute is empty.

Unfortunately, <progress> elements still require vendor-prefixed pseudo-elements. Here’s our CSS:

progress {

background: #ccc;

box-shadow: 0 0 8px 0px #000a;

border-radius: 1000rem;

display: block;

overflow: hidden;

width: 100%;


/* Firefox progress bars */ progress:indeterminate::-moz-progress-bar {

background: repeating-linear-gradient(-45deg, #999,           #999 1rem, #eee 1rem, #eee 2rem);


/* WebKit and Blink progress bars */ progress:indeterminate::-webkit-progress-bar {

background: repeating-linear-gradient(-45deg, #999,           #999 1rem, #eee 1rem, #eee 2rem);


/* Perhaps someday we’ll be able to do this */ progress:indeterminate {

background: repeating-linear-gradient(-45deg, #999,           #999 1rem, #eee 1rem, #eee 2rem);


This CSS gives us the progress bar shown below.

When the value of the progress element changes, it will no longer have an :indeterminate state.

:in-range and :out-of-range

The :in-range and :out-of-range pseudo-classes can be used with range, number, and date input form controls. Using :in-range and :out-of-range requires setting min and/or max attribute values for the control. Here’s an example using the number input type:


<label for=”picknum”>Enter a number from 1-100</label>

<input type=”number” min=”1″ max=”100″ id=”picknum” name= “picknum” step=”1″>


Let’s add a little bit of CSS to change styles if the values are within or outside of our range of one to 100:

:out-of-range {

background: #ffeb3b;


:in-range {

background: #fff;


If the user enters -3 or 101, the background color of #picknum will change to yellow, as defined in our :out-of-range rule.

Otherwise, it will remain white, as defined in our :in-range rule.

:valid and :invalid

With the :valid and :invalid pseudo-classes, we can set styles based on whether or not the form input meets our requirements. This will depend on the validation constraints imposed by the type or pattern attribute value. For example, an <input> with type=”email” will be invalid if the user input is “foo 123”, as shown below.

A form control will have an invalid state under the following conditions:

  • when a required field is empty
  • when the user’s input doesn’t match the type or pattern constraints—such as abc entered in an input[type=number] field
  • when the field’s input falls outside of the range of its min and max attribute values

Optional fields with empty values are valid by default. Obviously, if user input satisfies the constraints of the field, it exists in a valid state.

Form controls can have multiple states at once. So you may find yourself managing specificity (discussed in the next section) and cascade conflicts. A way to mitigate this is by limiting which pseudo-classes you use in your projects. For example, don’t bother defining an :optional rule set if you’ll also define a :valid rule set.

It’s also possible, however, to chain pseudo-classes. For example, we can mix the :focus and :invalid pseudo-classes to style an element only if it has focus: input:focus:invalid . By chaining pseudo-classes, we can style an element that has more than one state.


Where ::placeholder matches the placeholder text, the :placeholder-shown pseudo-class matches elements that currently have a visible placeholder. Placeholder text is typically visible when the form control is empty—that is, before the user has entered any information in the field. Any property that can be used with <input> elements can also be used with :placeholder-shown.

Remember that :invaLid matches form controls that have a required attribute and no user data. But we can exclude fields for which no data has been entered by combining :invalid with :not() and :pLacehoLder-shown :

input:not(:placeholder-shown):invalid {

background-color: rgba(195, 4, 4, .25);

border-color: rgba(195, 4, 4, 1);

outline-color:     rgba(195,4,4, 1);


The image below shows the results. Both form fields are required, but only the field with invalid user-entered data is highlighted.

Our first field is visually marked invalid because the user has entered an invalid email address. However, the second field hasn’t changed, because the user hasn’t entered data.

As mentioned earlier in this section, placeholder text can introduce usability challenges. For that reason, it’s best avoided. Removing the attribute, however, prevents us from using the :pLacehoLder-shown pseudo-class.

But there’s a simple fix. Set the value of the pLacehoLder attribute to a whitespace character: pLacehoLder=” “ . This lets us avoid the usability issues associated with using placeholder text, but still takes advantage of the :pLacehoLder-shown selector.

Source: Brown Tiffany B (2021), CSS , SitePoint; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *