Advanced Form Styling

appearance: controlling OS-level styling

In the previous article we said that historically, the styling of web form controls was largely taken from the underlying operating system, which is part of the problem with customizing the look of these controls.

The appearance property was created as a way to control what OS- or system-level styling was applied to web form controls. By far the most helpful value, and probably the only one you'll use, is none. This stops any control you apply it to from using system-level styling, as much as possible, and lets you build up the styles yourself using CSS.

For example, let's take the following controls:

<form>
  <p>
    <label for="search">search: </label>
    <input id="search" name="search" type="search" />
  </p>
  <p>
    <label for="text">text: </label>
    <input id="text" name="text" type="text" />
  </p>
  <p>
    <label for="date">date: </label>
    <input id="date" name="date" type="datetime-local" />
  </p>
  <p>
    <label for="radio">radio: </label>
    <input id="radio" name="radio" type="radio" />
  </p>
  <p>
    <label for="checkbox">checkbox: </label>
    <input id="checkbox" name="checkbox" type="checkbox" />
  </p>
  <p><input type="submit" value="submit" /></p>
  <p><input type="button" value="button" /></p>
</form>

Applying the following CSS to them removes system-level styling.

input {
  appearance: none;
}

The following live example shows you what they look like in your system - default on the left, and with the above CSS applied on the right (find it here also if you want to test it on other systems).

In most cases, the effect is to remove the stylized border, which makes CSS styling a bit easier, but isn't really essential. In a couple of cases - search and radio buttons/checkboxes, it becomes way more useful. We'll look at those now.


Taming search boxes

<input type="search"> is basically just a text input, so why is appearance: none; useful here? The answer is that Safari search boxes have some styling restrictions - you can't adjust their height or font-size freely, for example.

This can be fixed using our friend appearance: none;, which disables the default appearance:

input[type="search"] {
  appearance: none;
}

In the example below, you can see two identical styled search boxes. The right one has appearance: none; applied, and the left one doesn't. If you look at it in Safari on macOS you'll see that the left one isn't sized properly.

Interestingly, setting border/background on the search field also fixes this problem. The following styled search doesn't have appearance: none; applied, but it doesn't suffer from the same problem in Safari as the previous example.

Note: You may have noticed that in the search field, the "x" delete icon, which appears when the value of the search is not null, disappears when the input loses focus in Edge and Chrome, but stays put in Safari. To remove via CSS, you can use input[type="search"]:not(:focus, :active)::-webkit-search-cancel-button { display: none; }.


Styling checkboxes and radio buttons

Styling a checkbox or a radio button is tricky by default. The sizes of checkboxes and radio buttons are not meant to be changed with their default designs, and browsers react very differently when you try.

For example, consider this simple test case:

<label
  ><span><input type="checkbox" name="q5" value="true" /></span> True</label
>
<label
  ><span><input type="checkbox" name="q5" value="false" /></span> False</label
>

span {
  display: inline-block;
  background: red;
}

input[type="checkbox"] {
  width: 100px;
  height: 100px;
}

Different browsers handle the checkbox and span differently, often ugly ways:

Browser Rendering
Firefox 71 (macOS) firefox-mac-checkbox
Firefox 57 (Windows 10) firefox-windows-checkbox
Chrome 77 (macOS), Safari 13, Opera chrome-mac-checkbox
Chrome 63 (Windows 10) chrome-windows-checkbox
Edge 16 (Windows 10) edge-checkbox


Using appearance: none on radios/checkboxes

As we showed before, you can remove the default appearance of a checkbox or radio button altogether with appearance:none; Let's take this example HTML:

<form>
  <fieldset>
    <legend>Fruit preferences</legend>

    <p>
      <label>
        <input type="checkbox" name="fruit" value="cherry" />
        I like cherry
      </label>
    </p>
    <p>
      <label>
        <input type="checkbox" name="fruit" value="banana" disabled />
        I can't like banana
      </label>
    </p>
    <p>
      <label>
        <input type="checkbox" name="fruit" value="strawberry" />
        I like strawberry
      </label>
    </p>
  </fieldset>
</form>

Now, let's style these with a custom checkbox design. Let's start by unstyling the original check boxes:

input[type="checkbox"] {
  appearance: none;
}

We can use the :checked and :disabled pseudo-classes to change the appearance of our custom checkbox as its state changes:

input[type="checkbox"] {
  position: relative;
  width: 1em;
  height: 1em;
  border: 1px solid gray;
  /* Adjusts the position of the checkboxes on the text baseline */
  vertical-align: -2px;
  /* Set here so that Windows' High-Contrast Mode can override */
  color: green;
}

input[type="checkbox"]::before {
  content: "✔";
  position: absolute;
  font-size: 1.2em;
  right: -1px;
  top: -0.3em;
  visibility: hidden;
}

input[type="checkbox"]:checked::before {
  /* Use `visibility` instead of `display` to avoid recalculating layout */
  visibility: visible;
}

input[type="checkbox"]:disabled {
  border-color: black;
  background: #ddd;
  color: gray;
}

You'll find out more about such pseudo-classes and more in the next article; the above ones do the following:

  • :checked - the checkbox (or radio button) is in a checked state - the user has clicked/activated it.
  • :disabled - the checkbox (or radio button) is in a disabled state - it cannot be interacted with.

You can see the live result:

We've also created a couple of other examples to give you more ideas:

  • Styled radio buttons: Custom radio button styling.
  • Toggle switch example: A checkbox styled to look like a toggle switch.

If you view these checkboxes in a browser that doesn't support appearance, your custom design will be lost, but they will still look like checkboxes and be usable.