This is episode #21 in a series examining modern CSS solutions to problems I've been solving over the last 13+ years of being a frontend developer. Visit ModernCSS.dev to view the whole series and additional resources.
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.
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:
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:
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.
File Input CSS
Let's take another look at just our file input across Chrome, Safari, and Firefox:
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.
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:
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:
And here's the appearance for the readonly
field, since it has a different border-style
:
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.
Here is the solution with all the field types we covered represented.
Top comments (3)
Clean looking font in the cover image π
Is it Maiden by Deltatype? π
Ha, no it's Baloo 2, available on Google fonts π
Maybe I have missed something, but on iOS chrome and safari I do not see the box shadow on focus elements.
I also noticed some inner shadows.