CSS Pseudo-classes: Styling Form Fields Based on Their Input
The following is an extract from our book, CSS Master, written by Tiffany B. Brown. Copies are sold in stores worldwide, or you can buy it in ebook form here.
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 syle various types of form controls differently.
:enabled
and :disabled
As their name suggests, these pseudo-classes match elements that have (or lack) the disabled
HTML5 attribute. This can be an input control such as input
, select
, or button
element (seen shortly), or it can be a fieldset
element:
<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 is without a disabled
attribute set. Conversely, button:disabled
would match all button elements with a disabled
attribute:
button:disabled {
opacity: .5;
}
The figure 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.[6] For example:
<p>
<label for="email">E-mail:</label>
<input type="email" id="email" name="email" placeholder="example: jane.doe@example.com" required>
</p>
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 from above, and is shown in the figure below:
input:required {
border: 1px solid #ffc107;
}
The :optional
class works similarly, by matching elements that do not have a required
attribute. For example, the CSS that follows gives us the results seen below.
select:optional {
border: 1px solid #ccc;
}
:checked
Unlike the other pseudo-classes that 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.
Unfortunately, styling radio controls and checkboxes in most browsers is about as pleasant as a trip to the dentist for a filling. CSS Basic User Interface Module Level 4 attempts to address this with the appearance property, but this property is not yet supported. WebKit/Blink-based browsers and Firefox do, however, support nonstandard, vendor-prefixed versions of it.
In order to create custom radio button and checkbox inputs that work well across browsers, we need to become clever with our selectors. We’ll use a 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] { opacity: 0; }
[type=radio] + label::before {
background: #fff;
content: '';
display: inline-block;
border: 1px solid #444;
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 control. -
The form control must have an
id
attribute in addition to thename
attribute (for example,<input type="radio" id="chocolate" name="flavor">
). -
The
label
must have afor
attribute, and its value must match theid
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
).
:in-range
and :out-of-range
The :in-range
and :out-of-range
pseudo-classes can be used with range
, number
, and date
input 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:
<p>
<label for="picknum">Enter a number from 1-100</label>
<input type="number" min="1" max="100" id="picknum" name="picknum" step="1">
</p>
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;
}
Should the user enter -3 or 101, the background color of #picknum
will change to yellow as defined in our :out-of-range
rule set (see the figure below). Otherwise, it will remain white as defined in our :in-range
rule set.
: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 represented in teh figure below.
A form control will have an invalid state under the following conditions:
-
when a required field is an empty field
-
when the user’s input does not match the
type
orpattern
constraints -
when the field’s input falls outside of the range of its
min
andmax
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 while it has focus: input:focus:invalid
. By chaining pseudo-classes, we can style an element that has more than one state.
[6] Remember that in HTML5, the presence or absence of the attribute determines its value. In other words, required="false"
has the same effect as required="true"
, required="required"
and required
.