How to Build HTML Forms Right: Styling
Tags: a11y, css, design, html
Published: 2020-10-06 18:58:45

---
title: How to Build HTML Forms Right: Styling
published: true
date: 2020-07-13 03:56:22 UTC
tags: Accessibility,CSS,Design,HTML
canonical_url: https://austingil.com/build-html-forms-right-styling/
cover_image: https://austingil.com/wp-content/uploads/css-blog-cover.jpg
---

I’ve written a couple of articles now on how to write HTML forms for the modern web. The previous articles focused on semantics and accessibility which really are the foundation for any good form. If you’re interested in the overview of the entire series, here’s what it looks like:

- [Part 1: Semantics](https://austingil.com/how-to-build-html-forms-right-semantics/)
- [Part 2: Accessibility](https://austingil.com/how-to-build-html-forms-right-accessibility/)
- **Part 3: Custom Styles**
- Part 4: User Experience
- Part 5: Security

This will be part 3, and it’s probably the article I’m most excited for because we’re going to look at some common form design patterns, some of the gotchas, and how to approach them with CSS. Hopefully we won’t need to get **too** hacky and you’ll be able to walk away with some new knowledge and code snippets.

## Styling Forms is Not Great

As a web developer, you will eventually run into the scenario where you have to write custom styles for some form input. If you’re lucky, it will be a simple text field. These are pretty straight forward and non-trivial.

If you’re not lucky, however, you will need to style one of the not-so-friendly elements in MDN’s “[Advanced Form Styling](https://developer.mozilla.org/en-US/docs/Learn/Forms/Advanced_form_styling)” article. These are elements that have their own browser-provided styles which have little to no options to customize.

The list of troublesome input includes:

- `checkbox`
- `color`
- `datalist`
- `date`/`datetime`/`datetime-local`/`time`/`week`/`month`
- `file`
- `meter`
- `number`
- `radio`
- `range`
- `progress`
- `select`/`option`/`optgroup`
- `search`

That’s not a short list. Fortunately, we are going to look at solutions to cover the most common needs I think you will have. The one thing to remember before thinking about styling is to get the [semantics](https://austingil.com/how-to-build-html-forms-right-semantics/) and [accessibility](https://austingil.com/how-to-build-html-forms-right-accessibility/) correct **first!** (So read those articles if you haven’t already)

In general, I like to have some global styles applied to all my inputs, and a `visualy-hidden` utility class for accessible, but hidden content:

```css
input,
select,
textarea {
  width: 100%;
  max-width: 100%;
  font: inherit;
  color: inherit;
}

input[type="checkbox"],
input[type="radio"],
input[type="submit"],
input[type="button"] {
  width: auto;
}

fieldset,
legend {
  margin: 0;
  border: 0;
  padding: 0;
}

.visually-hidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  width: 1px;
  height: 1px;
  margin: -1px;
  border: 0;
  padding: 0;
}
```

The following examples will highlight only the critical points of markup and CSS, but I’ll also link to more practical examples that look nice.

## The Easier Ones

### Number

![HTML number input](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/number-input.png)<figcaption>Example of HTML number input from Chrome</figcaption>

The `number` input will be the simplest one to address. The only thing I’ve ever had to serch for is how to hide the little increment/decrement buttons you see when the input is selected.

Hiding them is very simple. It boils down to setting the `appearance` property Chrome to `none`, and to `textfield` in Firefox:

```css
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  appearance: none;
}
input[type="number"] {
  -moz-appearance: textfield;
}
```

### Select

![](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/select-chrome.png)<figcaption>Example of HTML &lt;select&gt; in Chrome</figcaption>

The `select` input will be more interesting. This is the first question I remember havng around custom styling for inputs: “ **How do you customize the arrow for a `select`?** “

The answer is actually quite simple, and requires two parts:

- First we hide the native dropdown by setting the `appearance` CSS property to `none`. Easy enough.
- Then we provide our own custom arrow/chevron. My preferred way is to use an encoded SVG as a CSS background image.

My reasoning for the encoded SVG is:

- Using a CSS background image will be applied to all `<select>` elements without needing custom markup like an inline SVG would.
- SVG will be crisper and possibly a smaller footprint than a PNG or JPG.
- I encode it because it’s small enough that it won’t bloat the CSS too much, and we avoid the extra HTTP request if we did a URL asset (no caching benefit though).

Finally, we’ll add a bit of padding to make sure the selected option does not overlap the background image. Here’s what the CSS looks like:

```css
select {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none; /* hides the native UI */

  padding-right: 1.5em; /* prevents input text from running into background image */

  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" fill="none" stroke="black"><path d="M7.5 3L4 6 .5 3"/></svg>');
  background-size: 0.7em;
  background-repeat: no-repeat;
  background-position: right 0.5em center;
}
```

Some more good news is that it’s quite easy to swap out whatever other SVG you want right in there as well, or change the color by adjusting the stroke. The bad news is that SVGs used as CSS background images cannot inherit the color of their context (using `currentColor` for example). So if you want to do something like that, you will either need to provide multiple class modifiers, or just use an absolute-positioned inline SVG.

See the Pen [https://codepen.io/Stegosource/pen/gOPKqox](https://codepen.io/Stegosource/pen/gOPKqox) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

This solution works fine for the initial display of the select, but it doesn’t let us customize the actual options. Unfortunately, `<option>` and `<optgroup>` elements cannot be styled much beyond colors. If you need that, you’ll have to build your own totally custom version. I’ll suggest the article “[<select> your poison](https://sarahmhigley.com/writing/select-your-poison/)” by [Sarah Higley](https://twitter.com/codingchaos) if you’re interested in going down that rabbit hole.

### Search

![Chrome HTML search input](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/search-chrome.png)<figcaption>Example of HTML search input in Chrome</figcaption>

The first thing I want to say about the `search` input is that you may not actually need it. `text` may be perfectly fine for your use-case, and in fact, Adrian Roselli has [a good article](https://adrianroselli.com/2019/07/ignore-typesearch.html) against using `search` at all. The following content still applies for `text` inputs that you want to style for search forms, however.

The `search` input is another relatively simple element to style, but there are a couple of things you should know. At first glance, it may look just like `<input type="text">`, but there are a couple of things to note. 1) for Chrome users, a `search` input will also render a cancel button to clear out the input, and 2) for those of you that REALLY care about every pixel, there is a slight variation to the spacing around a `search` input.

Beyond that, it’s not much different than your basic `text` input. So why spend time talking about it? Well, there are a couple of things you may want to consider.

If you want to write CSS that targets all search inputs across your site and style them differently than basic text inputs, you can do so with `input[type="search"]`. The styles could be very similar to our custom select above, but just swap out the SVG background image like so:

```css
input[type="search"] {
  padding-right: 1.5em;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black" stroke-width="2"><circle cx="7" cy="7" r="6" /><path d="M11 11 L15 15" /></svg>');
  background-size: 0.7em;
  background-repeat: no-repeat;
  background-position: right 0.5em center;
}
```

Something worth noting is that Chrome adds a small cancel button to search inputs. If you want to remove it, you can use the following CSS snippet:

```css
input[type="search"]::-webkit-search-cancel-button {
  display: none;
}
```

See the Pen [https://codepen.io/Stegosource/pen/zYraepg](https://codepen.io/Stegosource/pen/zYraepg) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

And there’s another important consideration for search forms. If you want to have a search form that only contains a text input and search icon, then the search icon could be an absolute-positioned submit button. This was discussed in my semantics and my accessibility posts. It could look like this:

```html
<form>
  <label for="search">
    <span class="visually-hidden">Search</span>
    <input type="search" name="search" id="search" />
  </label>
  <button type="submit">
    <span class="visually-hidden">Submit</span>
    <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#444" stroke-width="2px"><circle cx="7" cy="7" r="6" /><path d="M11 11 L15 15" /></svg>
  </button>
</form>
```
```css
form {
  position: relative;
}

input {
  padding-right: 1.5em;
}

button {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  border: 0;
  background: transparent;
}

button svg {
  width: 1em;
  height: 1em;
  vertical-align: middle;
}
```

Now, you may want to add your own classes in there, but what this snippet does is create a form with an input. The submit button is absolutely positioned to the right of the form, and vertically centered within the form. And the input has some padding on the right to prevent any text from running beneath the absolutely positioned button. We also add a `visually-hidden` class to make it easier to add some accessible text that isn’t actually displayed.

See the Pen [https://codepen.io/Stegosource/pen/NWxzPOv](https://codepen.io/Stegosource/pen/NWxzPOv) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
### Meter

![Firefox HTML meter](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/meter-firefox.png)<figcaption>Example of HTML meter in Firefox</figcaption>

![Chrome HTML meter](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/meter-chrome.png)<figcaption>Example of HTML meter in Chrome</figcaption>

The `meter` element is useful for visually representing a value relative to it’s minimum, maximum, and optimum values. It has 5 relevant attributes which can affect it’s UI: `min`, `low`, `max`, `high`, `optimum`, `value`. The default appearance is a small colored bar, and the color depends on these attributes. It’s a bit unintuitive at first, if you ask me.

Because you have 4 numeric attributes, you can have 3 ranges:

- `min` to `low`
- `low` to `high`
- `high` to `max`

The `optimum` value us a number that can sit within any of those ranges. If the `value` is in the same range, the `meter` will be green. If the value is in an adjacent range, the `meter` will be yellow. And if it’s farthest away (for example, optimum is between `min` and `low`, while value is between `high` and `max`) the `meter` will be red.

I hope that makes sense.

Let’s look at an example. We might have some school assignments with grades. Obviously the optimum value is on the higher end, so we’ll put it between `high` and `max`. Then we can have 3 meters representing a low, medium, and high grade.

```html
<label for="grade1">
  Grade 1
  <meter id="grade1" value="60" min="0" low="70" high="85" max="100" optimum="95"></meter>
</label>

<label for="grade2">
  Grade 2
  <meter id="grade2" value="80" min="0" low="70" high="85" max="100" optimum="95"></meter>
</label>

<label for="grade3">
  Grade 3
  <meter id="grade3" value="90" min="0" low="70" high="85" max="100" optimum="95"></meter>
</label>
```

Then we could say that if you get a lower grade, you get a pale red color, if you get a medium grade, you get a boring khaki color, and if you get a high grade, you get an exciting rainbow. The styles are pretty easy, it’s just a lot of vendor specific pseudo-elements:

```css
meter {
  width: 100%;
  height: 2rem;

  /* Firefox styles */ 
  border: 1px solid;
  background: none;
}

meter::-webkit-meter-bar {
  background: none;
  border-radius: 0;
}
meter::-moz-meter-bar {
  background-image: none;
}

meter::-webkit-meter-even-less-good-value {
  background: palevioletred;
}
meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
  background-color: palevioletred;
}

meter::-webkit-meter-suboptimum-value {
  background-color: khaki;
}
meter:-moz-meter-sub-optimum::-moz-meter-bar {
  background-color: khaki;
}

meter::-webkit-meter-optimum-value {
  background-image: linear-gradient( to right, red, yellow, lime, cyan, blue, rebeccapurple, magenta);
}
meter:-moz-meter-optimum::-moz-meter-bar {
  background-image: linear-gradient( to right, red, yellow, lime, cyan, blue, rebeccapurple, magenta);
}
```

Unfortunately, the implementation across browsers does not match, so you have to repeat the styles with the relevant vendor prefixes for Webkit and Gecko browsers. Even still, the display will look slightly different across browsers. Heights are treated differently, and what is called the “meter bar” is interpreted slightly differently, hence the different values and implementations for borders, radii, and `-webkit-meter-bar` vs. `-moz-meter-bar`.

See the Pen [https://codepen.io/Stegosource/pen/QWyaPdV](https://codepen.io/Stegosource/pen/QWyaPdV) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
### Progress

![Firefox HTML progress](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/progress-firefox.png)<figcaption>Example of HTML progress in Firefox</figcaption>

![Chrome HTML progress](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/progress-chrome.png)<figcaption>Example of HTML progress in Chrome</figcaption>

The `progress` element, as it’s name suggests, is used for telling users about their progress in a task. It has two states, `indeterminate` (default) and `deteminate`, and it has two attributes, `max` and `value`. In fact, it’s a very straight forward element:

```html
<label for="progress">
  Progress:
  <progress id="progress" max="100" value="60"></progress>
</label>
```

Styling it is also fairly straight forward, although you do have to account for some browser prefixes. For example, if you wanted your `progress` elements to have a grey background with a purple progress bar, it might look like this:

```css
progress[value] {
  border: 0;
  background-color: #bbb;
  -webkit-appearance: none;
}

progress[value]::-webkit-progress-bar {
  background-color: #bbb;
}

progress[value]::-webkit-progress-value {
  background: rebeccapurple;
}
progress[value]::-moz-progress-bar { 
  background: rebeccapurple;
}

progress:not([value]) {
  /* indeterminate styles */
}
```

Notice the use of `[value]` in the selectors. This is a useful differentiator between a `progress` element in a determinate state, and one in an indeterminate state. I’ve never had the need to style an indeterminate progress bar, but if you want to, the example is at the bottom for how you might do that.

See the Pen [https://codepen.io/Stegosource/pen/abdKXPv](https://codepen.io/Stegosource/pen/abdKXPv) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

It’s important to know there are differences across browsers. Firefox has a default border while Webkit browsers do not. In this case we remove it for consistency, but you could just as well add your own. One weird quirk is if you do not set some value for either the `backround` or the `border` properties, your `progress` element will display with the default UI even if you customize the pseudo-elements below. That is why we also set the `-webkit-appearance: none;`. We don’t actually need it in this example, but we would if we left the border and background as the default.

Finally, to set the progress bar’s color, we can use browser specific pseudo-elements `::-webkit-progress-value` and `::-moz-progress-bar`. However to fill up any of the remaining space, only Webkit provides a specific pseudo-element, `::-webkit-progress-bar`. For Gecko browsers, we can put a background color on the `progress` element itself.

## The Moderate Ones

### Checkbox & Radio

![Firefox checkbox and radio inputs](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/checkbox-radio-firefox.png)<figcaption>Example of HTML checkbox and radio inputs in Firefox</figcaption>

I’m not sure when the first custom `radio` or `checkbox` inputs were implemented, but in today’s internet, they are quite common. You can find several articles online, and most component libraries have in some form or another.

There challenge with custom checkboxes and radio inputs is there is no way to customize the native browser UI.

This leads many developers to use something like a `<div>` element instead, but this is a mistake. The `<input type="checkbox">` is semantically correct and has accessibility built in. It also requires that you write your HTML properly or you won’t get the right design.

Fortunately, for modern browsers there is a very simple solution: `appearance: none;`. This handy rule solves the problem above by visually hiding the native input UI, but unlike `display: none;`, it keeps the element accessible to screen readers and keyboard navigation. Sweet!

```html
<form>
  <fieldset>
    <legend>Operating System</legend>

    <label for="windows">
      <input id="windows" name="os" type="radio" value="windows">
      Windows
    </label>

    <label id="mac">
      <input id="os" name="os" type="radio" value="mac">
      Mac
    </label>

    <label id="linux">
      <input name="os" type="radio" value="linux">
      Linux
    </label>
  </fieldset>

  <fieldset>
    <legend>Favorite JS Frameworks</legend>

    <label for="angular">
      <input id="angular" name="ck-angular" type="checkbox" value="angular">
      Angular
    </label>

    <label for="react">
      <input id="react" name="ck-react" type="checkbox" value="react">
      React
    </label>

    <label for="vue">
      <input id="vue" name="ck-vue" type="checkbox" value="vue">
      Vue
    </label>

    <label for="svelte">
      <input id="svelte" name="ck-svelte" type="checkbox" value="svelte">
      Svelte
    </label>
  </fieldset>
</form>
```
```css
@supports (appearance: none) or (-moz-appearance: none) or (-webkit-appearance: none) {
  input[type="checkbox"],
  input[type="radio"] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    position: relative;
    display: inline-block;
    top: -0.1em;
    width: 1em;
    height: 1em;
    border: 1px solid;
    vertical-align: middle;
    transition: box-shadow 0.1s ease;
  }

  input[type="checkbox"]:checked,
  input[type="radio"]:checked {
    box-shadow: inset 0 0 0 .25em rebeccapurple;
  }

  input[type='radio'] {
    border-radius: 50%;
  }
}
```

The great thing about this code is it works with all native `checkbox` and `radio` inputs. It’s also quite open for customization. I chose to keep it relatively simple and just use an inset box-shadow to denote the “checked” state, but you have plenty of options: CSS images, background images, and even animations.

See the Pen [https://codepen.io/Stegosource/pen/QWyrgPG](https://codepen.io/Stegosource/pen/QWyrgPG) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

It’s also worth noting that I prefer to use `em` units here so that my checkbox/radio will scale up or down with my text. There are also three properties in there (`position: relative; top: -0.1em; vertical-align: middle;`) that are used to vertically center the input UI. You may need to fiddle with these as well, and unfortunately, it’s not a good time for `display: flex`.

The downside is `appearance: none` is [not supported on Internet Explorer](https://caniuse.com/#search=appearance%EF%BF%BD). That’s pretty far back at this point, so you may not need to worry about it. In case you do, we can use the `@supports (appearance: none)` block to [progressively enhance](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement) the experience for modern browser users. You can even accomplish a similar effect with just a little more markup, but I won’t go into that.

There was a very good article on [CSS Tricks](https://css-tricks.com/) called “[Custom Styling Form Inputs With Modern CSS Features](https://css-tricks.com/custom-styling-form-inputs-with-modern-css-features/)” by [Aaron Iker](https://twitter.com/aaroniker_me) that covered a lot of the same principles. It’s worth a read if you want other examples of this sort of thing.

### Switch

The “switch” input is also quite common to see around the internet these days. I think it was popularized by Apple, but it’s not actually an HTML element. In most cases, because it only has two states (on or off), it’s just a fancy `checkbox`.

```html
<label for="switch">
  <input id="switch" name="switch" type="checkbox" value="enabled" class="switch">
  Windows
</label>
```

With the one addition of `class="switch"`, we can actually use CSS very similar to the custom checkboxes to implement our “switch” input. Let’s assume we already have the CSS snippet above in our styles, so we only need to add the following:

```css
@supports (appearance: none) or (-moz-appearance: none) or (-webkit-appearance: none) {
  input[type="checkbox"].switch {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    position: relative;
    top: -0.1em;
    display: inline-flex;
    align-items: center;
    width: 2.6em;
    border: 1px solid currentColor;
    border-radius: 10000px;
    padding: .2em;
    margin-right: 2em;
    vertical-align: middle;
  }
  input[type="checkbox"].switch:before {
    content: '';
    width: 1em;
    height: 1em;
    border-radius: 10000px;
    background: hotpink;
  }
  input[type="checkbox"].switch:checked:before {
    background-color: limegreen;
    transform: translateX(1em);
  }
  input[type="checkbox"].switch:after {
    content: 'Off';
    position: absolute;
    right: -2em;
  }
  input[type="checkbox"].switch:checked:after {
    content: 'On';
  }
}
```

If you’ve been using CSS for a while may have noticed something unusual about this code. We are styling a `:before` pseudo-element on the `input`. Most `<input>` elements do not have the pseudo-elements `:before` or `:after`, `radio` and `checkbox` are of the few exceptions. Combining that with `appearance: none` let’s us hide the native UI, and apply our own.

We also add some visual text change to accompany the color change when the checkbox is checked. This is a good practice to account for color blind folks. The one caveat is that CSS does not play nicely with translation, so if you need to support multi-lingual sites, then you may need a bit more of a robust solution or stick with a regular checkbox.

See the Pen [https://codepen.io/Stegosource/pen/pogKYgM](https://codepen.io/Stegosource/pen/pogKYgM) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

Once again, we are relying on support for browsers newer than Internet Explorer, but if you need to support older browsers, there are still options for that. Just ask, and I can share. And once again, you may need to play with the values to get it as you like. Take note that you may need to play around with the `width`, `height`, `padding`, and/or `transform` of the `input` or the `:before` for things to line up properly, as they do relate to one another.

### Range

![Firefox HTML range](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/range-firefox.png)<figcaption>Example of HTML range input in Firefox</figcaption>

![Chrome HTML range](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/range-chrome.png)<figcaption>Example of HTML range input in Chrome</figcaption>

The `range` input is another one we got with the addition of HTML5. It provides a nice way for users to select a value between a `min` and `max` by dragging a little handle along a slider, which can be nicer than a plain old `number` input. The implementation is simple, and there are only a few attributes specific to this element.

```html
<label for="dogs">
  How much do you like dogs?
  <input type="range" id="dogs" name="dogs" min="9" max="10" step="0.1" value="10">
</label>
```

Once again, the implementation is straight forward enough, but the native UI across browsers is quite different so it makes sense to add your own styles. Here’s where things get a bit more involved. It’s not difficult; just verbose.

```css
input[type=range],
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  font-size: 1rem;
}

/* Track Styles */
input[type=range]::-webkit-slider-runnable-track {
  display: flex;
  align-items: center;
  border: 1px solid currentColor;
  border-radius: 3px;
  background-color: lavender;
}
input[type=range]::-moz-range-track {
  height: 1em;
  border: 1px solid currentColor;
  border-radius: 3px;
  background-color: lavender;
}

/* Thumb Styles */
input[type=range]::-webkit-slider-thumb {
  border: 1px solid currentColor;
  height: 1em;
  width: 1em;
  border-radius: 1000px;
  background: #fff;
}
input[type=range]::-moz-range-thumb {
  border: 1px solid currentColor;
  border-radius: 1000px;
  font-size: 1em;
  background: #fff;
}
```

Another tricky thing is that even using the pseudo-elements, there are slight variances in implementation we should be aware of. I’ll look at just Firefox and Chrome for now:

- In Chrome, to implementing custom styles for the thumb, we need to add `-webkit-appearance: none;` to both the input AND the slider thumb.
- In Chrome, the size of the track is based on the size of the thumb, unless you explicitly set it. In Firefox, the size of the track is defined by the native styles, unless you explicitly set it, and the size of the thumb has no impact on the size of the track.
- In Chrome, you cannot customize the thumb unless you follow the first bullet. In which case, you have to explicitly set the size. In Firefox, the size of the thumb is based on the font-size of the input, or you can set an explicit font-size or width and height.
- In Chrome, sine we customized the thumb, we can set the `display` of the track to `flex` to make it easier to vertically center. In Firefox, the thumb and track do not have that sort of relationship, but it also is inherently centered so there isn’t the need.
- In both browsers, the color of an input is not inherited unless you explicitly set `color: inherit`. I like to do this on all inputs although it’s not in the snippet above.

If that sounds complicated, that’s because it is. Most of the inconsistency is around getting things to size properly. So my preferred way of setting the size across browsers is to set the track size and thumb size based on `em` values, then simply rely on the input’s `font-size` to make any adjustments as needed.

I didn’t include vendor styles for Internet Explorer, but the good news is that there is some support. To style the track, you can use `input[type=range]::-ms-track`, and to style the thumb, `input[type=range]::-ms-thumb`.

See the Pen [https://codepen.io/Stegosource/pen/KKVoLaY](https://codepen.io/Stegosource/pen/KKVoLaY) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

If you want to go into further details, then I would recommend the article “[Styling Cross-Browser Compatible Range Inputs with CSS](https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/)” by [Daniel Stern](https://twitter.com/danieljackstern).

## The Difficult Ones

### Fancy Checkbox & Radio

Custom checkboxes and radios is great, but sometimes they can get quite complex. Take, for example, an input where you have to choose your started Pokemon:

![three starter pokemon options](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/pokemon-picker-1080x447.png)

From a semantics point of view, I think this is quite straight forward. You can only select one Pokemon which means it should be a `radio` input. The challenging thing here is with some of the technical and accessibility considerations. Ultimately we want a user to be able to click anywhere on the card to select that Pokemon, keyboard navigation should be supported, and we should account for options being focused and/or selected.

The solution I came up with is relies on the markup for each input to look like this:

```html
<label for="poke1">
  <input id="poke1" type="radio" name="selection" value="bulbasaur" aria-labeledby="poke1-name" aria-describedby="poke1-desc" class="visually-hidden" />
  <div class="card">
    <h3 id="poke1-name">Bulbasaur</h3>
    <div id="poke1-desc">
      <!-- description stuff here -->
    </div>
  </div>
</label>
```

- Wrap the entire option in a `<label>`. This makes the entire “card” clickable in order to select the `radio` option without the need for JavaScript.
- Place a “visually hidden” `radio` input right before the “card” element for styling purposes (this will be explained below).
- Use an `aria-labeledby` attribute to overwrite the label of the input. This is assigned to the Pokemon title. You could instead use `aria-label` if there is not an element that makes sense as a label.
- Use `aria-describedby` on the Pokemon description within the `<label>` so that the content is still accessible.

That covers our semantics, and most of our accessibility needs, however there are two things we still need to account for. Since the `input` is visually hidden, we lose any sort of visual feedback for the focus and selected options. Fortunately, the solution is quite simple:

```css
input:focus + .card {
  box-shadow: 0 0 0 .25rem mediumpurple;
}

input:checked + .card {
  color: #fff;
  background: indigo;
}
```

By placing the “card” element immediately after the `input`, we can take advantage of the [Adjacent Sibling Combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator%EF%BF%BD) (`+`) again to add special styles to the “card” element any time it’s `input` is either focused or checked.

See the Pen [https://codepen.io/Stegosource/pen/OJMEQrZ](https://codepen.io/Stegosource/pen/OJMEQrZ) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

A similar result can be achieved by putting the `input` inside the “card” and using the `:focus-within` pseudo-class, but `:focus-within` [is not supported in IE](https://caniuse.com/#search=focus-within) and I don’t know how to style the selected element without JavaScript.

### File

![Firefox file input](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/file-firefox.png)<figcaption>Example of HTML file input in Firefox</figcaption>

![Chrome file input](https://cdn.statically.io/img/stegosource.com/f=auto%2Cq=70/wp-content/uploads/file-chrome.png)<figcaption>Example of HTML file input in Chrome</figcaption>

There are a few reasons that make the file input a bit more challenging to work with.

- The native UI is a combination of a button and some text.
- Across different browsers, the implementation varies.
- Customizing the text that comes in the native UI is not trivial.

These are all good reasons to want to create your own custom UI, and some folks opt to build it without using the native input at all. I don’t like to do that because I think there is a lot that is lost in terms of semantics, accessibility, and even features in that case.

The native file input supports the following features. By incorporating the native file input, we’ll get some of this for free:

- Click to open file picker UI
- Drag and drop to select a file
- Support for `multiple` attribute
- Tab navigation support
- Visible focus state

Similar to the fancy card `radio`, this custom file selector also has some stricter requirements from the markup:

```html
<label for="file">
  <input id="file" type="file" multiple class="visually-hidden">
  <div class="dropzone">
    Select files
  </div>
</label>
```

We start with a native `file` input, and we give it the `visually-hidden` class. This way, visual users will not see the native input, but it will still be reachable through keyboard navigation. We then have a `div` with the “dropzone” class immediately after the input. It’s important that this `div` is a sibling of the input, and because they are both inside the `label`, the text will serve as the input label, so let’s be sure that it makes sense. If you want to change it to something that doesn’t make as much sense, you can always put an `aria-label` on the input, but keep this `div` as a sibling.

Next, let’s throw some more CSS in there to make the `div` look the part of a dropzone. Feel free to play around with the styles here as you see fit. I’ve just made something with a dashed border, a bit extra height, and with some centered text.

```css
.dropzone {
  display: flex;
  justify-content: center;
  align-items: center;
  border: 2px dashed currentColor;
  border-radius: .25rem;
  height: 3rem;
}

.dropzone--droppable {
  background: lightblue;
}

input:focus + .dropzone {
  outline-width: 1px;
  outline-style: auto;
  outline-color: Highlight;
  outline-color: -webkit-focus-ring-color;
}
```

The `.dropzone--droppable` part will make more sense in a moment, but for the time being, take a look at the last block. Our file input is visually hidden, so there is no feedback when a keyboard user focuses on the input. To account for that, we use the CSS [adjacent sibling combinator](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator) (`+`) to target the “dropzone” and add some focus styles to that. The rules I have here mimic the native browser focus ring, but you can use whatever outline styles you want. Just be sure to include some sort of visual cue.

Lastly, the hard part is to recreate the file drop feature. We made it this far without the need for any JavaScript, but we will reach for it here to get us over the finish line.

There are a few events we will need to attach event listeners to: `dragover`, `dragenter`, `dragleave`, and `drop`. We need `dragover` simply to prevent the default behavior. Otherwise, when we drop the files, the browser may navigate away. `dragenter` and `dragleave` are useful just for toggling some CSS classes so that we can provide some visual feedback. Finally, the real work of handling the file drop happens in `drop`.

```js
const input = document.querySelector('input[type="file"]')
const dropzone = document.querySelector('.dropzone')

function doSomethingWithFiles(files) {
  console.log('Doing something...', files)
}

dropzone.addEventListener('dragover', (event) => {
  event.preventDefault();
});

dropzone.addEventListener('dragenter', (event) => {
  event.stopPropagation();
  event.target.classList.add('dropzone--droppable')
});

dropzone.addEventListener('dragleave', (event) => {
  event.stopPropagation();
  event.target.classList.remove('dropzone--droppable')
});

dropzone.addEventListener('drop', (event) => {
  event.preventDefault();
  event.target.classList.remove('dropzone--droppable')

  const files = Array.from(event.dataTransfer.files);
  if (!input.multiple && files.length > 1) {
    files.length = 1;
  }
  
  doSomethingWithFiles(files)
});

input.addEventListener('change', (event) => {
  const files = Array.from(event.target.files)

  doSomethingWithFiles(files)
});
```

The `drop` event handler isn’t actually finished here because we aren’t doing anything with the files. That will be up to you to fill out the `doSomethingWithFiles` function. The good news is the dropzone event and the input change event will both behave the same way. You can send the files to an upload API and get back the URLs. Or you might keep them in memory until the form is submitted. You might even read the file data and respond to it by showing an image preview, or importing the contents.

Here’s the catch, one thing you cannot do is attach the files to the form. `file` inputs are read-only, which means even though we can intercept the dropped files, we cannot insert them into the input’s value attribute. This is a good thing for security reasons, but it also means that our form now requires JavaScript for this one feature to work.

The good news is that the file data can be saved in memory just fine and sent along with an AJAX request just as if you would send an input anyway. And the input itself still works just fine without JavaScript by either using the keyboard to navigate to the input, of clicking on the “dropzone” since that is the label. If you absolutely must allow for the file drop feature on the input, then it’s quite easy to implement JavaScript detection. Then you could show the input and hide the dropzone if JavaScript is disabled.

See the Pen [https://codepen.io/Stegosource/pen/VwerogQ](https://codepen.io/Stegosource/pen/VwerogQ) by Austin ([@Stegosource](https://codepen.io/Stegosource)) on [CodePen](https://codepen.io).

<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>

Some things that this implementation has not accounted for is updating the UI to tell the user that the input has changed. As stated before, we can’t assign the files to the input value, so we would need to rely on updating based on the JavaScript handlers alone to update the UI. We also probably would want a way to update and/or clear the selected files.

### The Rest

I’m won’t be covering the remaining elements. `date` inputs and `color` are far too complicated to add to this article. They would really require their own dedicated post to get into all the nuance around styling and accessibility. As for `datalist`, it’s quite straight forward to style the input, but the selectable options suffer the same issues as the `<option>` tag in that they cannot be styled.

## Styling States

Inputs can have many states: `required`, `disabled`, `focused`, `invalid`. The good news is that if you write your code semantically, the browser can communicate these states, keeping things nice and accessible. However, the browser may or may not provide visual cues about these states, so you will want to account for them in your styles.

### Required & Optional

HTML5 input validators make it easy enough for us to mark an input field as required by simply adding the `required` attribute:

```html
<input name="example" required>
```

However, for visually able users that are not relying on assistive technology, it’s helpful to include some sort of cue to denote required fields. The most common I’ve seen is a red asterisk, and you can achieve that by adding the character to your HTML, or with CSS.

If you prefer the HTML approach, you will be doing your screen reader uses a favor by wrapping it with an `aria-hidden` attribute so it is not read out loud.

```html
<label for="example">
  Label text <span class="color-red" aria-hidden="true">*</span>
  <input id="example" name="example" required>
</label>
```
```css
.color-red {
  color: red;
}
```

If you choose to add that asterisk with the CSS `content` property, you can also avoid it being read by screen readers by providing an empty alt attribute, or setting the `speak: never;` rule.

```html
<label for="example">
  <span class="required-label">Label text</span>
  <input id="example" name="example" required>
</label>
```
```css
.required-label {
  content: '*' / '';
  speak: never;
  color: red;
}
```

Lastly, if you only want to apply styles to the input (not elements before it), you can use the `:required` and/or `:optional` pseudo-selectors.

```css
input {
  border-left-color: blue;
}

input:optional {
  border-left-color: yellow;
}

input:required {
  border-left-color: red;
}
```

### Disabled

The `disabled` attribute tells browsers and assistive technology that an interactive element is disabled (duh). It’s pretty straight forward, and so are my preferred styles.

```html
<input name="example" disabled>
```
```css
input:disabled,
select:disabled,
textarea:disabled,
button:disabled,
a:disabled {
  opacity: 0.7;
  cursor: not-allowed;
}
```

Browsers provide their own styling for disabled inputs, but chances are you are not using the native browser styles. In those cases, you should make sure to account for disabled elements. I like to do this by adding opacity and a `not-allowed` cursor. The opacity usually gives enough visual distinction without having to rely on specific colors.

### Focused

For visually-abled folks using keyboard navigation, we always want to account for the focus state. I’ve shown some ways to do that in a few of the examples, but I want to point out this snippet specifically:

```css
input:focus + .next-sibling {
  outline-width: 1px;
  outline-style: auto;
  outline-color: Highlight;
  outline-color: -webkit-focus-ring-color;
}
```

It covers a few key points.

1. If we ever want to target an element based on whether it has focus or not, we can use the CSS `:focus` pseudo-selector.
2. To target sibling elements we can reach for CSS [combinators](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors/Combinators), specifically `+` or `~`.
3. The styles for this rule show how to imitate the browser’s native focus ring. Although you could just as well use a fully custom one if you prefer.

The golden rule to keep in mind about focus states is that you should always account for it. Never remove the default outline unless you replace it with **some** visual change. I like to use `box-shadow` because it sticks to the edge of the element and respects the `border-radius`, unlike `outline`.

### Valid & Invalid

The last states to cover are for valid and invalid inputs. They can be very useful for giving a user feedback on whether they are filling out a form properly or not. Fortunately, CSS provides use with some more pseudo-classes, `:valid` and `:invalid`.

Let’s say, for example, that you wanted to add a red bottom border for any invalid inputs:

```css
input:invalid,
select:invalid,
textarea:invalid {
  border-bottom-color: red;
}
```

Conversely, you may decide to only add a green bottom border for valid inputs:

```css
input:valid,
select:valid,
textarea:valid {
  border-bottom-color: green
}
```

[Alligator.io](https://alligator.io/) has a very nice article with more examples called “[Styling Form Inputs in CSS With :required, :optional, :valid and invalid](https://alligator.io/css/styling-form-input-validity/)“. However, the biggest issue I have with this is that the feedback is immediate. I personally prefer to have a chance to fill out the input before being told whether it’s valid or not. If you wanted to do that, you would likely want to reach for JavaScript and listen for the `blur` event on each input, or the `submit` event on the form and then highlight the invalid inputs.

## Closing Remarks

Wow! That was a lot. Hopefully you found it useful, and if you have any comments, recommendations, or corrections, please let me know.

I think the key takeaways are:

- Focus on semantics and accessibility before getting into the design.
- The `visually-hidden` class is a useful utility in any project.
- The `appearance` property and CSS combinators can get you very far. 
- Remember to test all the different inputs and all their different states.
- And always try to use what the platform provides for consistent and accessible experiences.

Finally, it would really help me if you would take a moment to [share this article](https://twitter.com/intent/tweet?url=https%3A%2F%2Fstegosource.com%2Fbuild-html-forms-right-styling%2F&text=Check%20out%20this%20article%20by%20%40Stegosource%20about%20design%20patterns%2C%20common%20gotchas%2C%20and%20CSS%20tips%20and%20snippets%20for%20building%20HTML%20forms.). I worked very hard on it and would appreciate it greatly. If this is the sort of thing you’d like to see more regularly, you should also [subscribe to my newsletter](https://austingil.com/newsletter) and [follow me on Twitter](https://twitter.com/stegosource).