Forms have always been notoriously difficult to style with CSS, but there are several little-used selectors that give us significant power to style inputs and surrounding elements. Some of these are relatively new, while others have been available for quite some time.

Check out the live demo here.

:placeholder-shown

Input with label that hides and shows based on the visibility of the placeholder

The first selector is relatively new and doesn’t have complete browser support yet. However, this seems like something that could easily work as a progressive enhancement. The selector allows us to detect whether a placeholder is currently visible to the user. This could come in handy if we want to dynamically hide and show the input’s associated label.

Here I am hiding the label until the user types in the input, thus hiding the placeholder. I use a nice transition effect to display the label. Note that for this to work, the label must come AFTER the input.

<div class="form-group">
    <input type="text" id="dynamic-label-input" placeholder="Enter some text">
    <label for="dynamic-label-input">Enter some text</label>
</div>
.form-group {
    position: relative;
    padding-top: 1.5rem;
}

label {
    position: absolute;
    top: 0;
    font-size: var(--font-size-small);
    opacity: 1;
    transform: translateY(0);
    transition: all 0.2s ease-out;
}

input:placeholder-shown + label {
    opacity: 0;
    transform: translateY(1rem);
}

:required

Required input with help text

Use this selector to indicate that an input has the required attribute. Here I am using an empty .help-text span and placing some content dynamically using the ::before pseudo-element. Realistically, this would be done with JavaScript, but I am including here to demonstrate a pure CSS approach.

<label for="required-input">Required input</label>
<input type="text" id="required-input" required>
<span class="help-text"></span>
input:required + .help-text::before {
    content: '*Required';
}

:optional

Optional input with help text

This selector does the opposite of :required. I am again using an empty .help-text span to display some optional text if the required attribute is NOT present.

input:optional + .help-text::before {
    content: '*Optional';
}

:disabled

Disabled input

This one should be familiar to most of you, but still important to remember. It’s pretty essential to display whether or not an input is disabled to a user.

&:disabled {
    border-color: var(--gray-lighter);
    background-color: var(--gray-lightest);
    color: var(--gray-light);
}

:read-only

Read-only input

An input with the readonly attribute should convey a slightly different meaning than a disabled input. Thankfully we have this selector to help with that.

<input type="text" value="Read-only value" readonly>
input:read-only {
    border-color: var(--gray-lighter);
    color: var(--gray);
    cursor: not-allowed;
}

:valid

Valid input with CSS success styling

While much form validation will happen with JavaScript, we are able to take advantage of HTML5 form validation and style inputs accordingly. This selector gives us the chance to style any input which is currently passing the native browser validation rules.

Here I am encoding an svg to display a checkbox in the input using the background-image property.

input:valid {
    border-color: var(--color-primary);
    background-image: url("data:image/svg+xml,%3Csvg width='45px' height='34px' viewBox='0 0 45 34' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg transform='translate%28-56.000000, -59.000000%29' fill='%232EEC96'%3E%3Cpolygon points='70.1468531 85.8671329 97.013986 59 100.58042 62.5664336 70.1468531 93 56 78.8531469 59.5664336 75.2867133'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
}

:invalid

Invalid input with CSS error styling

This selector checks if an input is currently NOT passing the native browser validation rules (for example, if an email input does not contain a real email).

Again, I am encoding an svg to display a ‘x’ in the input.

input:invalid {
    border-color: var(--color-error);
    background-image: url("data:image/svg+xml,%3Csvg width='30px' height='30px' viewBox='0 0 30 30' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg transform='translate%28-128.000000, -59.000000%29' fill='%23F44336'%3E%3Cpolygon points='157.848404 61.9920213 145.980053 73.8603723 157.848404 85.7287234 154.856383 88.7207447 142.988032 76.8523936 131.119681 88.7207447 128.12766 85.7287234 139.996011 73.8603723 128.12766 61.9920213 131.119681 59 142.988032 70.8683511 154.856383 59'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
}

I can also customize some validation messages for each input type using the .help-text span and the ::before pseudo-element.

<label for="invalid-email">Invalid input</label>
<input type="email" id="invalid-email" value="notanemail">
<span class="help-text"></span>
input[type='email']:invalid + .help-text::before {
    content: 'You must enter a valid email.'
}

:in-range/:out-of-range

Out-of-range input with CSS error styling

These selectors detect whether the value of a number input is within the min/max values specified or not.

<label for="out-of-range-input">Out-of-range input</label>
<input type="number" id="out-of-range-input" min="1" max="10" value="12">
<span class="help-text"> (value must be between 1 and 10)</span>
input:out-of-range + .help-text::before {
    content: 'Out of range';
}

:checked

Checkbox with CSS checked styling

Most of you will be familiar with this selector. It gives us the ability to apply custom styles to checkboxes and radio buttons when checked. My technique for styling checkboxes involves creating a wrapper element and placing the label after the input.

<div class="checkbox">
    <input type="checkbox"/>
    <label>Option</label>
</div>

I visually hide the input so that it disappears from view but is still clickable. Then I style label::before to look like the checkbox input and label::after to look like a checkmark. I use the :checked selector to style these two pseudo-elements appropriately:

&:checked + label::before {
    background-color: var(--color-primary);
}

&:checked + label::after {
    display: block;
    position: absolute;
    top: 0.2rem;
    left: 0.375rem;
    width: 0.25rem;
    height: 0.5rem;
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
    content: '';
}

Getting a Head Start with the Hi-Q Framework

My new CSS framework Hi-Q provides some useful base styling for inputs, including checkboxes, radio buttons, and disabled inputs.

You can use the theme builder to visually watch the changes you are making to inputs as you make them.

Hi-Q is lightweight and is built with CSS variables that can be changed dynamically, especially useful for theme switching.