Ashley Sheridan​

Accessible Styled Form Elements

Posted on


Since the inception of CSS, there's been one area of styling that has been a constant issue; forms. Simple elements, like <input type="text"> and <textarea> and buttons, have generally been ok, as borders, colours and fonts could all be set with standard styles that behaved as expected. Beyond that though, other elements were impossible to style consistently without either compromising on the design, or attempting to recreate the native elements with a lot of custom Javascript. Neither are great as they leave you with a form that either doens't look good, or doesn't work well in terms of accessibility.

Usually, the approach was just to make sure that everything looked nice, and accessibility was an afterthought. I've even worked at places where I was told to focus on the design aspect first and foremost, and that accessibility wasn't a major issue because most visitors could see perfectly fine, depsite me trying to persuade them otherwise.

Just over a year ago though, browsers started implementing pseudo form elements (all their own non-standard prefixes, of course!) which allowed for CSS to more specifically target parts of a complex form element. Finally, elements could be styled to a far greater degree, and without the need for lots of custom HTML tags around every element (a sure sign of divitis!) and without lots of custom Javascript capturing various events to emulate standard form element behaviour.

I put this together to help as a guide to making accessible form elements that are nicer to look at than the standard stuff, and aside from some minor Javascript on the file upload and colour picker elements, everything is achieved with pure CSS, and basic HTML. An aside of this was to ensure that complex HTML setup wasn't required, as sometimes we really don't get the chance to change the underlying HTML source because it's either from a 3rd party or is part of a larger shared project of its own.

The Elements

I've tried to keep the CSS standalone for each element here, but there's no reason why you can't combine styles into a set of shared classes for elements that have commonalities, or even chuck the whole thing into a SASS component file.

  1. Select lists
  2. Text/Password inputs
  3. Checkboxes (inside <label>)
  4. Checkboxes (immediate sibling of <label>)
  5. File uploads
  6. Colour pickers
  7. Sliders/range elements

Select Lists

Modern browsers now support the appearance CSS property (or a variation of) that allows the arrow, background, etc, of a select list to be unset completely:

<label>Simple Select <select class="simple_select"> <option value="1">something here</option> <option value="2">another thing with a long name</option> <option value="3">final thing</option> </select> </label> .simple_select { -moz-appearance: none; -webkit-appearance: none; appearance: none; background-color: #fff; background-image: url('down.png'); background-position: right center; background-repeat: no-repeat; border: 2px solid #ccc; border-radius: 10px; font-size: 16px; padding: 5px 25px 5px 5px; } .simple_select::-ms-expand { display: none; } .simple_select:focus { box-shadow: 0 0 6px 0 #0070b0; outline: none; }

The end result looks like this:

The key part here is the appearance property, along with the browser prefixed versions and Microsofts ::-ms-expand pseudo element selector. This allows you to change the default appearance of items. This is for IE10 and up (including Edge). IE9 doesn't support it, but you can use conditional comments on that platform to reset any other styles which may rely on the arrow being hidden.

It's important to note the focus styles here, as they are an accessibility must-have. Normally, a browser would add an outline which is typically blue (but may vary depending on the browser, OS, and desktop theme in use), but you don't get a lot of say about how that looks, and Chrome has a wonderful bug of drawing outlines with square corners, even if the element has rounded corners. Another thing worth noting is the extra padding on the right for the select list, which is to prevent the option text from overlapping the background arrow image.

One thing I would have liked to do is to change the background image depending on the status of the selects menu being shown/hidden. Unfortunately, I found no way of doing this, and using the focus style to achieve this is not recommended, as a select having focus doesn't necessarily mean its corresponding drop-down is visible.

Text Inputs

Styles here are pretty much as you're used to, with the same focus styles mentioned above.

<label>Your name <input type="text" class="simple_input" placeholder="Joe Bloggs"/> </label> <label>Your password <input type="password" class="simple_input"/> </label>

Again, focus styles are important, and they should be differentiate the form element enough that it's obvious where the browsers focus is. Accessibility is not just about making your website available to blind users, there are many people who aren't blind but are not able to use a mouse. They can still see the page and changes on it, but their method of navigation might not use a pointer device.

One interesting addition in the CSS here is for changing the character (slightly) used for password fields. Currently only Webkit-based browsers support this, but more may follow in the future:

.simple_input { background-color: #fff; border: 2px solid #ccc; border-radius: 10px; font-size: 16px; padding: 5px 25px 5px 5px; } .simple_input:focus { box-shadow: 0 0 6px 0 #0070b0; outline: none; } .simple_input[type=password] { -webkit-text-security: square; }

The final result looks like this:

Checkboxes Inside Label

Up until now, all of the styles have just been attached to the form element in question. Checkboxes are a little bit more tricky, however, as browsers don't allow pseudo :before and :after elements to be attached directly to the <input> element. The most typical way around this is by attaching the pseudo element to the label. I've included two ways of doing this, one with the label around the element, and another with it separate but linked via the id attribute.

<label class="nested_checkbox">Tick for something good <input type="checkbox"/> <span></span> </label>

The major immediate downside of this approach is that an extra element is required. This is unavoidable, as there is nothing to attach to here other than the label, which is a parent element to the checkbox, and thus can't be targeted with CSS (yet).

.nested_checkbox input[type=checkbox] { clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute; width: 1px; } .nested_checkbox input[type=checkbox]+span { border: 2px solid #ccc; display: inline-block; height: 15px; position: relative; width: 15px; } .nested_checkbox input[type=checkbox]:checked+span::after { bottom: 0; color: #f00; content: "✓"; font-size: 22px; left: .1em; line-height: 22px; position: absolute; } .nested_checkbox input[type=checkbox]:focus+span { box-shadow: 0 0 6px 0 #0070b0; }

The technique here is simple, the checkbox itself is hidden in a way that still allows it to be focused, so it retains accessibility, but visually the <span> does all the work. If you do go down the route of using characters as your visual tick of check as I have here, you will need to take care that your CSS is saved as a UTF document, and either served with UTF headers or using an @charset rule at the top of the CSS.

Checkboxes As Sibling Of Label

This might seem more familiar, as I've noticed a trend of keeping form elements outside of their corresponding labels, so it might be what you're already doing. The only downside is that this makes dynamic checkboxes more difficult to implement (as they all need unique id values), e.g. where you have x rows of a table and a checkbox for each that triggers some kind of action when the form is submitted.

<input type="checkbox" id="custom_checkbox" class="detached_simple_checkbox"/> <label for="custom_checkbox">Tick for something else</label>

The CSS is much the same as the previous example, just with an extra pseudo element to draw the box (previously that was just a styled <span> but the label here has content.

.detached_simple_checkbox { clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute; width: 1px; } .detached_simple_checkbox+label { padding-left: 1.6em; position: relative; } .detached_simple_checkbox+label:before { border: 2px solid #ccc; bottom: 0; content: ""; display: inline-block; height: 15px; left: .1em; width: 15px; position: absolute; } .detached_simple_checkbox:checked+label:after { bottom: .1em; color: #f00; content: "✓"; font-size: 22px; left: .3em; line-height: 22px; position: absolute; } .detached_simple_checkbox:focus+label:before { box-shadow: 0 0 6px 0 #0070b0; }

The end result is this:

Which looks exactly like the previous example, just achieved in a different way.

File Uploads

This is something that is very tricky, and still not possible to style nicely without using Javascript for some minor functionality. The label wrapping the element here allows the span that's acting as a visual button to trigger the file browse dialogue, but the same could be achieved by moving the input element before the label and using sibling selectors in CSS. I've also added a data-file attribute here, which is important later for showing the selected file.

<label>Simple custom file upload <input type="file" class="simple_file_upload"/> <span data-file="">Choose file...</span> </label>

First, the input element itself is hidden in the standard accessible way, and the <span> is styled to appear like a button:

.simple_file_upload { clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute; width: 1px; } .simple_file_upload+span { background-color: #a8d6f1; border: 2px solid #0070b0; border-radius: 10px; display: block; font-size: 16px; margin-top: .5em; padding: 5px; position: relative; width: 150px; }

Next, is a pseudo element which will show the value of our data-file attribute. If you're looking to develop for very old browsers that don't support this syntax, then you could use a second <span> instead and have the Javascript directly edit it's contents, but the method here means you're using as few HTML tags as possible.

.simple_file_upload+span:after { bottom: 5px; content: attr(data-file); display: block; font-size: 15px; font-style: italic; left: 170px; margin-top: .6em; position: absolute; width: 300px; }

Finally, there are the focus styles. Due to a bug in Firefox, the file input itself doesn't receive focus; instead it's a pseudo element which does, but one that can't be targeted with CSS. Because of this, some Javascript can be added to handle the focus events and toggle a class on the input element.

.simple_file_upload:focus+span, .simple_file_upload.focus+span { box-shadow: 0 0 6px 0 #0070b0; }

The Javascript to accompany all of this is split into two parts, one to handle changing the filename selected in the spans data-file attribute, and one to handle the focus/blur events:

document.addEventListener('DOMContentLoaded', function(){ var simple_file_upload = document.getElementsByClassName('simple_file_upload')[0]; simple_file_upload.addEventListener('change', function(){ this.nextElementSibling.setAttribute('data-file', this.value); }); // fix for Fx as focus goes to the button part of the input which is classes as a sort of pseudo element simple_file_upload.addEventListener('focus', function(){ simple_file_upload.classList.add('focus'); }); simple_file_upload.addEventListener('blur', function(){ simple_file_upload.classList.remove('focus'); }); });

The end result is something like this:

Colour Pickers

Not a particularly popular form element, but as they exist and have decent browser support, I thought I'd have a go at seeing how well they could be styled in an accessible way. As I wanted to be able to have my own colourised box with rounded corners, I'm going with the extra <span> tag to show this colour, as it allows for full control over the styling. I've set a specific colour here because Chrome has a bug where the lightness level for the colour is set to 0 by default, which can make it appear to users that the colour picker isn't working correctly, so this aids accessibility.

<label>Choose colour: <input type="color" class="simple_colour" value="#ff0000"/> <span></span> </label>

The CSS is very similar to standard text fields, with the addition of some styling to the extra <span> tag. If you're setting the input itself to have a different starting value than the default (which is black) then you should update the value in the CSS to match. If this isn't possible (e.g. the CSS is in an external stylesheet file) then you could use a line in Javascript code to update it once the document loads, as the styled element already has Javascript for setting the colour of the span.

.simple_colour { clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute; width: 1px; } .simple_colour+span { display: block; width: 30px; height: 30px; background-color: #f00; border: 2px solid #ccc; border-radius: 10px; } .simple_colour:focus+span { box-shadow: 0 0 6px 0 #0070b0; }

The Javascript code listens to the input event, which fires any time the colour value is changed, which is not necessarily when the focus leaves the field. In-fact, this fires many times as you change the colour while the picker dialogue is open.

document.addEventListener('DOMContentLoaded', function(){ var simple_colour = document.getElementsByClassName('simple_colour')[0]; simple_colour.addEventListener('input', function(){ = this.value; }); });

Sliders/Range Elements

Range elements are quite a new addition to the HTML spec and give you a way of picking a numerical value using a nice visual mechanism. With the addition of the element came a lot of (browser-specific, of course) pseudo elements that you can style to completely change the appearance of the slider.

First, is our old friend appearance, which allows us to override the default styles for the element, and we can set a focus style for the whole thing as well. The background transparency fixes a minor bug found in some versions of browsers when the track (the bar in which the slider moves) is thinner than the slider thumb (the part that moves).

Note that there is no CSS here to remove the border. That won't be necessary, and will cause problems with accessibility if the browser your visitors are using doesn't support <input type="range"/>, because the default behaviour for unrecognised inputs is to behave as a text input, and without a border, it's hard to see where the field is unless you're focus is on it.

.simple_slider { -moz-appearance: none; -webkit-appearance: none; appearance: none; background: transparent; width: 200px; } .simple_slider:focus { box-shadow: 0 0 6px 0 #b00070; outline: none; }

After this, the CSS gets a little messy, as it seems that Chrome and Firefox have issues if their range pseudo selectors are mixed with any other selector. So for example, this works:

.simple_slider::-webkit-slider-runnable-track {...}

But this does not work:

.simple_slider::-webkit-slider-runnable-track, .simple_slider::-moz-range-track {...}

Each range element has two (or four in the case of IE/Edge, because of course it has to be different!) sub-elements: the track, and the slider thumb. These can be styled individually of each other like. So, to style the track for the main browsers using all prefixes:

.simple_slider::-webkit-slider-runnable-track { background-color: #a8d6f1; height: 6px; } .simple_slider::-moz-range-track { background-color: #a8d6f1; height: 6px; } .simple_slider::-ms-track { height: 6px; width: 100%; }

IE/Edge is the only browser that ignores any background colour you set here, as it has a further two sub-elements which are the track parts to the left and right of the slider, and each has to be coloured separately. It also needs a width set to 100% to use the full element width, where the other browsers default to this. If you're using SASS, you can achieve the above with a mixin or function, so that you don't have large chunks of CSS which are essentially copies of each other.

To style the extra pseudo elements for IE/Edge, you'll need the following added:

.simple_slider::-ms-fill-lower { background: #a8d6f1; } .simple_slider::-ms-fill-upper { background: #a8d6f1; }

With these alone, you should have a solid bar of colour, so now you'll need to set up the styles for the slider. In this, at least, all the browsers I tested seem to be (mostly) consistent in their presentation.

.simple_slider::-webkit-slider-runnable-track { background-color: #a8d6f1; height: 6px; } .simple_slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; background-color: #0070b0; border: 0; border-radius: 50%; cursor: pointer; /* margin top should be roughly (h / 5 * 2) * -1 , where h is height of thumb */ margin-top: -8px; height: 20px; width: 20px; } .simple_slider::-moz-range-thumb { -moz-appearance: none; appearance: none; background-color: #0070b0; border: 0; border-radius: 50%; cursor: pointer; height: 20px; width: 20px; } .simple_slider::-ms-thumb { background-color: #0070b0; cursor: pointer; height: 20px; margin-top: 0; width: 20px; }

Chrome has an odd bug where the vertical position of the slider is wrong if the sliders height is larger than the tracks height. This is easily fixed with a negative vertical margin, which is roughly (h / 5 * 2) * -1 , where h is height of slider thumb. Edge has an even stranger bug, where it actually picks up some of the Webkit values for this and applies the negative margin offset to it's own slider! To fix this, I added a specific reset of the margin-top to 0

The end result is this:


The form elements examples here work in all the standard browsers, including Chrome, Edge, Firefox, and Opera. Hopefully this article shows that you don't need to sacrifice accessibility for the design, or vice-versa. One thing to remember though, is that the built in form elements will always have better accessibility support than anything you can reproduce using Javascript. Even if you think you've covered every possible angle, there will be someone else who comes along with issues that you never foresaw. It's best to leave the accessibility implementation to browser vendors, rather than attempt to reinvent the wheel for every website you work on. Designing and developing with accessibility in mind should take an enhancement approach, not a create-from-scratch approach, and if you remember that, you should be able to produce great looking stuff that works for just about everyone, all with little effort.