Styling Selects Purely with CSS

We've struggled with how to style native selects, well, pretty much forever. The problem has always been that browsers haven't given us the ability to consistently style the various parts of a select and menu with CSS. That is still largely true today. As a result, if you want a select that matches a particular design across a wide range of browsers, your only choice is to use JavaScript to create a custom component.

In our early attempts to build a custom select, we generated the select button and menu popup in JavaScript by grabbing the values from a native select element we accessibly hid off-screen. Although effective, we've found that re-creating all the functionality of a native select is surprisingly difficult. Whenever you replace a native element with a custom Javascript widget, you must fully reproduce every feature and take care to get things like keyboard shortcuts and accessibility support working as well as the real thing to do it right and that adds code complexity. It also turns out all that JavaScipt code can have serious performance drawbacks on mobile phones or older browsers so in recent years we've reserved this approach for special cases where the menu required complex custom styling such as thumbnails or complex data formatting for the options.

More recently, we've been using a lightweight approach that we open sourced as our custom select widget. This works by placing a native select element that has been styled to be completely invisible sitting on top of a custom-styled element. When a user taps on the custom select button, they are actually tapping the invisible select which triggers the native menu to open. This is a pretty common technque that many popular sites on the web now use because of it's simplicity and ability to offer consistent visual control. However, it requires javascript to update the text feedback in our custom select button to match the currently selected option. It's not much code, but it adds a depedency on JavaScript we need to think about.

This custom select plugin has served us well over the last few years but it's 2014 and decided to re-visit our old frienemy, the native select. Could we style selects purely with CSS to avoid the need for JavaScript? After a week of trolling through StackOverflow posts and doing a lot of trial-and-error in our device lab, we think we have a very solid cross-browser, CSS-only custom select that pulls together all the best techniques and CSS hacks we've found around the web into one place.

View demo (in JSBin)

Browser Test Results

Note that all these are screenshots from Browserstack but we also tested thoroughly on real devices in our lab to ensure the menus worked and focus styles were clear on each device.

Screenshots of the custom CSS select in al popular browsers

The technique

In a nutshell: this technique works by removing all the native select element's appearance except for the text that shows the currently selected option. We then wrap the select in a container and apply all the custom select styling we want there to avoid the quirks and limitations of trying to directly style native select elements. Lastly, a pseudo element adds the right hand arrow to finish up our custom select.

In Chrome, Safari, iOS Safari, Android, and other Webkit-based browers like newer Blink-based Opera versions, it really is that easy. However, Firefox, IE, and older Opera browsers make things a much more complicated. We needed find creative ways to target very specific styles to each of these browsers to make them behave and hide the native select styling.

With our progressive enhancement mindset, the overall goal is to be able to style select elements consistently in as many browsers as possible, but also ensure that the technique crisply degrades to a simple native select in less capable browers so we don't break the core functionality with our newfangled CSS.

As you can see from the final browser testing results below, we think we achieved our goal. Only a handful of browsers fall "in between" and display both the the native select arrow and our custom arrow together but it's fairly benign visual bug and only happens in very obscure browser versions.

If you want to just copy and paste this code and start styling, view the code and demo (JSBin). If you're interested in the CSS hackery involved, read on for the gory details.

How it works

The markup for our custom styled select starts with a select wrapped in a container with the class of custom-select. As with any form element, always include a label for accessibility, either by wrapping label around the select, or stacking and associating the label with a for attribute that matches the ID of the select as shown.


<label for="fruits">Pick a fruit</label>

<div class="custom-select button">
  <select id="fruits">
    <option>Apples</option>
    <option>Bananas</option>
    <option>Grapes</option>
    <option>Oranges</option>
    <option>A very long option name to test wrapping</option>
  </select>
</div>
   

First we style the native select to be virtually invisible except for the text of the currently selected value. We do this by applying both the -webkit prefixed and standard appearance: none properties, setting the width to 100% of the container and removing any margins, borders, background, etc. Fortunately, padding can be set consistently cross-browser on the select once appearance has been set to none to align the text in the custom select.

.custom-select select {
  width:100%;
  margin:0;
  background:none;
  border: 1px solid transparent;
  outline: none;
  /* Prefixed box-sizing rules necessary for older browsers */
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  /* Remove select styling */
  appearance: none;
  -webkit-appearance: none;
  /* Magic font size number to prevent iOS text zoom */
  font-size:16px;
  /* General select styles: change as needed */
  font-family: helvetica, sans-serif;
  font-weight: bold;
  color: #444;
  padding: .6em 1.9em .5em .8em;
  line-height:1.3;
}
  

Next, we add the styles for the dropdown wrapper which are very simple. The position: relative; rule creates a positioning context so we can absolutely position the custom arrow image. In this example, the button class on this wrapper has all the custom button styles that you will customize: gradient, shadow, border, text color, etc.

.custom-select {
  position: relative;
  display:block;
  margin-top:0.5em;
  padding:0;
}

Lastly, we add in a pseudo element for the custom arrow on the right of our select button to make this look like a select menu. In this example we're using a 2x PNG for the arrow to keep the code simple, but this could alternatively be a SVG, icon font, or multiple bitmaps targeted for normal and high DPI screens via a media query.

Note that this arrow element sits on top of the native select because of the z-index:2 rule so tapping the arrow will prevent the menu from opening and might be confusing to our visitors. To address this, we add a pointer-events:none; rule to make a tap on the arrow open the select even though it is beneath the arrow. Note that since the pointer-events trick won't work everywhere, keeping the arrow image small relative to the select will minimize this drawback.

     
.custom-select::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 8px;
  top: 50%;
  right: 1em;
  margin-top:-4px;
  background-image: url(http://filamentgroup.com/files/select-arrow.png);
  background-repeat: no-repeat;
  background-size: 100%;
  z-index: 2;
  pointer-events:none;
}

At this point, we have a very consistent custom select that works in all Webkit-based browsers like iOS Safari, Android Browser and Chrome, Chrome Desktop, and Safari Mac. Now we need to work on getting all the other browsers on board.

Internet Explorer

Internet Explorer has a clear divide: older versions (6-9) don't support a way to style the selects reliably, but newer versions (10-11) do so we needed a way to precisely target this divide. Luckily, there is clever CSS hack that gives us the ability target the proprietary IE select::-ms-expand rule to hide the native select styling in IE 10+.

IE has a default blue background color and white text for the select that looks odd combined with our custom select style so we remove it with the select:focus::-ms-value property.

Older versions of IE simply see a native select. We don't bother with targeting a rules to hide the custom arrow in IE 9 or older because the custom arrows don't seem to appear so it's one less hack.

/* IE 10/11+ - hide native select arrow in favor of custom select styles */
_:-ms-input-placeholder, :root .custom-select select::-ms-expand {
    display: none;
}

/* Removes the odd blue bg color behind the text in the select button in IE 10/11 */
_:-ms-input-placeholder, :root .custom-select select:focus::-ms-value {
    background: transparent;
    color: #222;
}

Opera

Opera's Presto browser never supported any form of appearance: none so our only choice is sticking with a native select. All we need to do is target a rule to hide the custom arrow on Opera. This hack works on Opera 9 and above, older versions will have a double arrow.

/* Opera - Pre-Blink nix the custom arrow, go with a native select button */
x:-o-prefocus, .dropdown::after {
  display:none;
}    

With Opera's switch to the Blink-based rendering engine in version 15, it gains custom select styling automatically because it picks up the same styles that Chrome does. That was easy.

Firefox

Firefox has a long, complex relationship with appearance: none. Firefox shipped with the prefixed -moz-appearance property in version 5. However, later versions of Firefox broke support for -moz-appearance: none; and warned that support was coming to an end. Undeterred, folks found clever workarounds that used a combination of text-indent and text-overflow to nudge the native select's arrow just a bit to hide it from view. By wrapping those rules in a CSS hack, it's possible to target this ugliness only to certain versions of Firefox:

@-moz-document url-prefix() {
  select {
    -moz-appearance: none;
    text-indent: 0.01px;
    text-overflow: "";
  }
}

That worked for a while, but Firefox eventually stopped respecting -moz-appearance: none; in later versions so the native arrow started popping up again. Undeterred, someone figured out that using the very hacky -moz-appearance: window would again reliably hide the select appearance in Firefox. The downside with using window is it interferes with the outline styling we want for clear focus visual feedback. In my demo, I've worked around this by adding a border and text color shift on focus so Firefox at least gets some clear form of focus feedback.

I had thought we were out of the woods by this stage, but as I did final testing, I noticed that Firefox 30+ on Windows again stopped working with this pile of hacks. After some digging, I discovered this is a known issue and found an article explaining the two possible workarounds: either set the select width to 110% and clip the arrow with overflow: hidden on the wrapper, or position a pseudo element over the native arrow to cover it up. Neither is a great choice, but the former option seems less bad and doesn't require effort to ensure the button and "coverup"" layer's background colors/gradients line up. The only downside to the 110% width hack is when the menu opens is 10% wider than normal. Not a showstopper, but c'mon Firefox.

So where did we land? Well, in Firefox 5 and earlier, we go with a native select. All we need to do is target a hack to old Firefox to hide the custom arrow and remove the right padding on the select to make thigs look correct.

/* Firefox >= 2 */
/* Show only the native arrow */
body:last-child .dropdown::after, x:-moz-any-link {
  display: none;
}
/* reduce padding */
body:last-child .dropdown select, x:-moz-any-link {
  padding-right: .8em;
}

In Firefox 6+, we target styles that override the older Firefox rules to upgrade the select to a fully custom-styled widget. Firefox 6 is the only quirky version where both the custom and native arrows appear together but this is a tiny sliver of Firefox users and it doesn't impact usability.

/* Show the custom arrow again */
_::-moz-progress-bar, body:last-child .custom-select:after {
   display: block;
}    

/* Hide the native select appearance */
_::-moz-progress-bar, body:last-child .custom-select select {
  -moz-appearance: window;
  text-indent: 0.01px;
  text-overflow: "";
  /* increase padding to make room for menu icon */
  padding-right: 13%;
}
  

Finally, we have no other choice but to use clipping to make Firefox 30+ behave but we target it in htis media query so older versions of Firefox don't have to see with extra wide menus. This looks good all the way up to version 32 beta (and hopefully beyond). You made me do this ugly thing, Firefox. Shame on you.

Note: as we were finalizing this article, it looks like Firefox 32 on Android 4.4 regressed even more and seems to require an even wider select width of 120% to clip off the native arrow. I'll leave it to reader how far they want to push this hack for the sake of Firefox's terrible rendering.

@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal) {
  /* Clip select with the container */
  _::-moz-progress-bar, body:last-child .custom-select {
      overflow: hidden;
  }    
  
 /* Make select extra wide so it clips off */
  _::-moz-progress-bar, body:last-child .custom-select select {
     width: 110%;
  }
}

We also found that Firefox applies a strange dotted outline around the text of the select which can be negated by this rule.

/* Firefox focus has odd artifacts around the text, this fixes that */
  select:-moz-focusring {
    color: transparent;
    text-shadow: 0 0 0 #000;
}

If all this CSS "finesse" makes you uncomfortable, you can choose to omit these Firefox rules and stick with a native select in Firefox. For those determined to make custom styles work, at least there is a solid (albeit hacky) way to do this. You should consider commenting on this Mozilla issue to encourage them to implement a sensible way to style selects. We'll still have 30+ old versions of Firefox to deal with but it would be a good step forward.

Wrapping up

Although not for the faint of heart, we've found this combination of techniques results in a remarkably consistent look and feel across all popular desktop and mobile browsers and has clean fallbacks to a simple native select when advanced styling isn't supported. Being able to use a native select menus is more plces means leaner, faster code with better accessibility and no JavaScript dependencies. We're hoping to try this in some of our upcoming projects and encourage you to share your ideas, bugs, and help us refine this technqiue.

Because this technqiue uses a lot of CSS hacks and proprietary properties, we'll need to keep testing this as new versions are released. Down the road, we will add styles for more future-friendly approaches like the shadow DOM that give us a more standards-friendly way to style elements.