Custom CSS Styles for Form Inputs and Textareas

We're going to create custom form input and textarea styles that have a near-identical appearance across the top browsers. We'll specifically style the input types of text, date, and file, and style the readonly and disabled states.

Read on to learn how to:

  • reset input styles
  • use hsl for theming of input states
  • ensure all states meet contrast requirements
  • retain a perceivable :focus state for Windows High Contrast mode

Now available: my egghead video course Accessible Cross-Browser CSS Form Styling. You'll learn to take the techniques described in this tutorial to the next level by creating a themable form design system to extend across your projects.

This is the fourth installment in the Modern CSS form field mini-series. Check out episodes 18-20 to learn how to style other common form field types including radio buttons, checkboxes, and selects.

Common Issues with Native Input Styles

There is a bit more parity between text input styles than we saw with radios, checkboxes, and selects, but inconsistencies nonetheless.

Here's a screenshot of the unstyled inputs we're going to address today across (from left) Chrome, Safari, and Firefox.

native input fields including text, date, file, and readonly and disabled states in the aforementioned browsers

We will be looking to unify the initial appearance across browsers and common field types.

The date field is unique in that Chrome and Firefox provide formatting and a popup calendar to select from, while Safari offers no comparable functionality. We cannot create this in CSS either, so our goal here is to get as far as we can with creating a similar initial appearance. Check out the caniuse for date/time inputs.

Base HTML

We're covering a lot of field types, so check the CodePen for the full list. But here is the essential HTML for a text input and a textarea.

<label for="text-input">Text Input</label>
<input class="input" id="text-input" type="text" />

<label for="textarea">Textarea</label>
<textarea class="input" id="textarea"></textarea>

To allow simplifying our styles and preparing to work with the cascade, we've only added one CSS class - input - which is placed directly on the text input and textarea.

The label is not part of our styling exercise, but its included as a general requirement, notably with the for attribute having the value of the id on the input.

Create CSS Variables for Theming

For the tutorial, we're going to try a bit different technique for theming by using hsl values.

We'll set a grey for the border, and then break down a blue color to be used in our :focus state into its hsl values, including: h for "hue", s for "saturation", and l for "lightness".

:root {
  --input-border: #8b8a8b;
  --input-focus-h: 245;
  --input-focus-s: 100%;
  --input-focus-l: 42%;
}

Each of the tutorials for our form fields has incorporated a bit different method for theming, which can all be extracted and used beyond just forms!

Accessible Contrast

As per all user interface elements, the input border needs to have at least 3:1 contrast against it's surroundings.

And, the :focus state needs to have 3:1 contrast against the unfocused state if it involves something like changing the border color or, according to the WCAG 2.2 draft, a thickness greater than or equal to 2px.

The draft for WCAG 2.2 makes some slight adjustments to :focus requirements, and I encourage you to review them.

Reset Styles

As is included in all my tutorials as a modern best practice, we add the following reset first:

*,
*::before,
*::after {
  box-sizing: border-box;
}

As seen in the initial state of the fields across browsers, some standout differences were in border type, background color, and font properties.

Interestingly, font-size and font-family do not inherit from the document like typography elements do, so we need to explicitly set them as part of our reset.

Also of note, an input's font-size should compute to at least 16px to avoid zooming being triggered upon interaction in mobile Safari. We can typically assume 1rem equals 16px, but we'll explicitly set it as a fallback and then use the newer CSS function max to set 16px as the minimum in case it's smaller than 1em (h/t to Dan Burzo for this idea).

.input {
  font-size: 16px;
  font-size: max(16px, 1em);
  font-family: inherit;
  padding: 0.25em 0.5em;
  background-color: #fff;
  border: 2px solid var(--input-border);
  border-radius: 4px;
}

We set our border to use the theme variable, and also created a slightly rounded corner.

After this update, we're already looking pretty good:

updated input styles in Chrome, Safari, and Firefox which all show the inputs with unified grey borders and white backgrounds

It may be difficult to notice in that screenshot, but another difference is the height of each field. Here's a comparison of the text input to the file input to better see this difference:

text input field across browsers compared to file input

Let's address this with the following which we are applying to our .input class as long as it is not placed on a textarea:

.input:not(textarea) {
  line-height: 1;
  height: 2.25rem;
}

We included line-height: 1 since when it's not a textarea it's impossible for an input to be multiline. We also set our height in rem due to considerations of specifically the file input type. If you know you will not be using a file input type, you could use em here instead for flexibility in creating various sized inputs.

But, critically, we've lost differentiation between editable and disabled input types. We also want to define readonly with more of a hint that it's also un-editable, but still interactive. And we have a bit more work to do to smooth over the file input type. And, we want to create our themed :focus state.

Join my newsletter for article updates, CSS tips, and front-end resources!

File Input CSS

Let's take another look at just our file input across Chrome, Safari, and Firefox:

current state of the file input styling across browsers

We cannot style the button created by the browser, or change the prompt text, but the reset we provided so far did do a bit of work to allow our custom font to be used.

We'll make one more adjustment to downsize the font just a bit as when viewed with other field types the inherited button seems quite large, and font-size is our only remaining option to address it. From doing that, we need to adjust the top padding since we set our padding up to be based on em.

input[type="file"] {
  font-size: 0.9em;
  padding-top: 0.35rem;
}

If you were expecting a fancier solution, there are plenty of folx who have covered those. My goal here was to provide you a baseline that you can then build from.

readonly CSS Style

While not in use often, the readonly attribute prevents additional user input, although the value can be selected, and it is still discoverable by assistive tech.

Let's add some styles to enable more of a hint that this field is essentially a placeholder for a previously entered value.

To do this, we'll target any .input that also has the [readonly] attriute. Attribute selectors are a very handy method with wide application, and definitely worth adding to (or updating your awareness of) in your CSS toolbox.

.input[readonly] {
  border-style: dotted;
  cursor: not-allowed;
  color: #777;
}

In addition to swapping for a dotted border, we've also assigned it the not-allowed cursor and enforced a medium-grey text color.

As seen in the following gif, the user cannot interact with the field except to highlight/copy the value.

the user mouses over the readonly field and is show the not-allowed cursor, and then double-clicks to highlight the value

Disabled Input and Textarea Style

Similar to readonly, we'll use an attribute selector to update the style for disabled fields. We are attaching it to the .input class so it applies on textareas as well as our other input types.

We'll make use of our CSS variable to update the border color to a muted grey, and the field background to a very light grey. We'll also again apply the not-allowed cursor as just an extra hint that the field is not interactive.

.input[disabled] {
  --input-border: #ccc;

  background-color: #eee;
  cursor: not-allowed;
}

And here is the result for both a text input and a textarea:

Alt Text

Accessibility Note: disabled fields are not necessarily discoverable by assistive tech since they are not focusable. They also are not required to meet even the typical 3:1 contrast threshold for user interface elements, but we've kept with user expectations by setting them to shades of grey.

Textarea Styles

Our textarea is really close, but there's one property I want to mention since it's unique to the inherent behavior of textareas.

That property is resize, which allows you to specify which direction the textarea can be resized, or if it even can at all.

While you definitely should allow the textarea to retain the resize function under general circumstances, you can limit it to just vertical resizing to prevent layout breakage from a user dragging it really wide, for example.

We'll apply this property by scoping our .input class to when it's applied on a textarea:

textarea.input {
  resize: vertical;
}

Try it out in the final CodePen demo!

:focus State Styles

Ok, we've completed the initial styles for our inputs and the textarea, but we need to handle for a very important state: :focus.

We're going to go for a combo effect that changes the border color to a value that meets 3:1 contrast against the unfocused state, but also adds a box-shadow for a bit of extra highlighting.

And here's why we defined our theme color of the focus state in hsl: it means we can create a variant of the border color by updating just the lightness value.

First, we define the border color by constructing the full hsl value from the individual CSS variable values:

.input:focus {
  border-color: hsl(var(--input-focus-h), var(--input-focus-s), var(--input-focus-l));
}

Then, we add in the box-shadow which will only use blur to create essentially a double-border effect. calc() is acceptable to use inside hsla, so we use it to lighten the original value by 40%, and also allow just a bit of alpha transparency:

.input:focus {
  /* ...existing styles */
  box-shadow: 0 0 0 3px hsla(var(--input-focus-h), var(--input-focus-s), calc(var(--input-focus-l) +
          40%), 0.8);
}

Note that we've now added a new context for our contrast, which is the :focus border vs. the :focus box-shadow, so ensure the computed difference for your chosen colors is at least 3:1 if using this method.

Optionally, jump back up to the .input rule and add a transition to animate the box-shadow:

.input {
  /* ...existing styles */
  transition: 180ms box-shadow ease-in-out;
}

Finally, we don't want to forget Windows High Contrast mode which will not see the box-shadow or be able to detect the border color change. So, we include a transparent outline for those users:

.input:focus {
  outline: 3px solid transparent;
}

We also use this technique in the episode covering button styles.

Here's a gif demo of focusing into the text input:

keyboard focusing into and out of the text input

And here's the appearance for the readonly field, since it has a different border-style:

the readonly field when focused

In the CodePen HTML, there is a comment with an example of using an inline style to define an updated visual such as for an error state. Again, keep in mind that we are lightening the provided --input-focus-l value by 40%, and the focused border color must be at least 3:1 contrast against the unfocused color, so consider that when you alter the CSS variable values.

Input Mode and Autocomplete

There are two aditional attributes that can help improve the user experience, particularly on mobile, in addition to using the correct input type (ex: email).

The first is defining the inputmode, which provides an altered keyboard or keypad that better matches the expected data. Read up on available inputmode values on MDN >

Second is autocomplete which has far more options than on or off. For example, I always appreciate that on iPhone when Google sends me a confirmation code by text the keyboard "just knows" what that value is. Turns out, that's thanks to autocomplete="one-time-code"!

Check out the full list of autocomplete values that allow you to hint at the value expected and really boost the user experience of your forms for users that make use of auto-filling values.

Demo

First, here's a final look at our solution across (from left) Chrome, Safari, and Firefox. The file input still sticks out a bit when viewed side by side, but in the flow of a form on an individual browser it's definitely acceptable.

final input and textarea styles across the aforementioned browsers

Here is the solution with all the field types we covered represented.

By Stephanie Eckles (@5t3ph)