Styleable select box replacement in pure CSS and semantic HTML

Ross Angus
7 min readMar 3, 2017

--

Let’s get this out of the way first: select boxes are a bad idea. Styling select boxes is a bad idea. Web browsers will never support full CSS styling of native select boxes. Briefly, this is because select boxes are considered to be native operating system controls — much like the “chrome” around the outside of your browser. Which means they can do stuff like this:

Select boxes can escape from the browser canvas, under certain circumstances (brrrr)

Change how they look and users no longer know what they’re looking for.

Good. Wait, you still want to style them? FINE.

There are some JavaScript libraries out there which can help — the confusingly named Chosen, jQuery selectBox, SelectBoxIt or Select2, for example. Some of these add useful features, such as search. But they all have one thing in common: they don’t take long to implement, but there’s always issues with them during the User Acceptance Testing phase of a project. Many of these problems arise from the fact that all of these libraries hide the original select box and duplicate it with other HTML elements, such as anchors or input tags. This means that events added to the original select box often get lost and user actions are not properly recorded.

What if we could do the same thing in pure, sexy CSS, using native HTML elements which make sense semantically?

What even is a select box?

There’s a few different kinds of select boxes:

In terms of functionality, all of these, apart from the “multiple” type, work exactly like radio buttons: if the user selects one option, then changes their mind, their original choice is deselected. As far as the server-side code is concerned, the user’s made a choice. It doesn’t care how that choice is made.

So most select boxes are just radio buttons?

Yup — all the server gets in the post of the form is name-value pairs. It doesn’t know or care which control is used to capture them from the user.

Select box replacement 1 (spoiler: it doesn’t work)

My initial thought was “what if I put a bunch of radio buttons and label tags inside a container, then made the container expand, when one of them achieved focus?” This would work as follows:

  • The radio buttons themselves would be hidden away visually
  • The label tags would be stacked on top of each other, using position: absolute
  • The container would also need to be absolutely positioned, so that when it expands, it sits on top of any following content
  • As soon as one radio button achieved focus, the position of all the label elements within the container would switch to static. As I've also changed their display property earlier to block, they would stack on top of each other, and force the container to take their full, natural height

I did all that, and this happened:

The main issue is that it’s impossible to actually select any value. I assume this is due to the blur event (for want of a better name) occurring before the focus on the label element selects the next radio button.

But there’s another more serious issue. The rule which does most of the work is this:

input:focus ~ label {position: static;}

This reads in English as “If an input element is put into focus, then change all of the following label tags to position: static". The ~ is a following sibling selector. There is no following and proceeding selector in CSS, and it's unlikely there ever will be, unless you use flexbox. I think this is because CSS thinks in terms of parsing down the DOM, from top to bottom (this is the same reason there's not a parent selector in CSS). It's not as agile as JavaScript, which scampers about the whole tree like a chimp.

Attempt two: the old toggle

The technique of using a checkbox to show and hide content, by using the adjacent sibling selector has been around for a while. My initial thought was to use that to toggle the select box open and closed. It wasn’t ideal — after the user had selected an option inside the select box, the box would not close until they hit the toggle again.

This worked, but I wondered if I could improve it.

I tried the shifting focus trick again: as soon as the user selected an option inside the select box, the focus should shift to that radio button and the select would close. Unfortunately this didn’t work probably for exactly the same reason the first attempt didn’t work: something about focus being lost before the radio button’s state can be changed. I dunno.

Attempt three: all radio buttons, all of the time

What if the select toggle itself was part of the same series of radio buttons as the rest of the select box? This time, the key rule is this:

.radio-toggle:checked ~ .select label {position: static;}

In English, this rule reads “if an element with a class of radio-toggle is checked, find all the following elements with a class of select, then change the position of all the label elements inside it to static".

This also means that if that same checkbox is no longer checked, the label elements revert to their previous position value of absolute (which stacks them up again). Demo:

I forgot one thing

The original, pure CSS version of this select box replacement has a terrible flaw: it’s only partially accessible via the keyboard. This issue was highlighted to me by the Reddit user jestho, whose one downvote sunk my post from sight.

But of course, jestho is absolutely correct: keyboard navigation is absolutely vital for such controls. I did what I could in pure css, but all I could manage to do is to add an outline around the select box, when it first comes into focus. Unfortunately, once the user selects a value from within the select box, moves onto a different control, then returns to the same select box, the highlight no longer displays (this is because in order to do so, changes on a child element would need to bubble up to the parent element, which ain't going to happen).

And of course, capturing keystrokes and allowing special navigation within an individual select box is completely outside of CSS’s jurisdiction. Which means it’s time for:

Attempt four: pure stylable CSS select boxes, now with added JavaScript!

Most of the JavaScript is there to capture and interpret different keystrokes, such as:

  • Cursor keys (moving up and down — this mostly comes for free, but the JavaScript stops focus at the end and the start of the range of values)
  • Page up (moves a configurable distance up, or to the first value, if applicable)
  • Page down (as above, but down)
  • Space (opens the select box)
  • Enter (toggles the select box open and closed)
  • Escape (closes the select box)

The use of tab is captured by events attached to changes in focus, so I didn’t need to write any jQuery to look out for that.

JavaScript keyboard navigation is a controversial subject, so I’ve attempted to replicate the behaviour of the default (Windows, sorry) select box. For keyboard navigation in other page elements, I’d suggest looking at the WAI-ARIA Authoring Practices, or if you have beef with the W3C for whatever reason, The A11Y Project.

I dislike the hard-coded nature of a lot of this JavaScript (including mine!). Ideally, keyboard navigation should have some kind of solver which has other functions which understands concepts such as first, last, next, previous etc. and can be added to any DOM structure with a few parameters. But that’s a whole other project.

Disadvantages of this technique

  • Because the .select element is positioned absolutely, it needs to appear inside another element which will leave space for it in the normal document flow
  • The .select element is effectively display: block: it will take the full width of whatever element you put it inside. Normal select elements are effectively display: inline-block and will take the width of the currently selected option element. But because we're removing the currently selected element from the document flow, we don't know its size.
  • Normal select elements are somewhat aware of the browser viewport and even the edges of the screen. This means that if required, they can expand to such a degree that they escape the browser itself and sit on top of it (see the image at the top of this post). It’s this behaviour which gives them their reputation of being part of the operating system, rather than part of the web page. The fake select box is confined by the root element of the page and cannot escape from it. It always takes the full height it needs, in order to display all of it’s options (unless you want to put a height and overflow on it).
  • Because of the exciting CSS used, this will only work on IE9 and up (if you need to support browsers lower than IE9, I suggest loading Sizzle in conditional comments inside the head).
  • Mobile users will never see the native select box style which takes over most or all of their screen. Note that some of the JavaScript libraries work-arounds for this is to disable their functionality in certain devices (if you need a similar effect for your select box, I’ve made a version which fills the whole viewport, but only for small screens).
  • If the select box needs to wrap, it’ll overlap any content which follows it.
  • Keyboard navigation is only partial currently — the select box will show focus when the user first tabs into it, but once a value is selected and focus returns to the select, there is no user indication of this (unless you add the JavaScript)

--

--

Ross Angus

The views I express here are mine alone and do not necessarily reflect the views of my employer.