Created
September 16, 2014 08:32
-
-
Save orioltf/e18e9405bf3c0e263b2c to your computer and use it in GitHub Desktop.
// source http://jsbin.com/qupebo/1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>JS Bin</title> | |
<style> | |
body { font-family: Helvetica; line-height: 1.3em; padding: 1em 10%; } | |
</style> | |
</head> | |
<body> | |
<h1>Styling Selects Purely with CSS</h1> | |
<p>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 <code>select</code> that matches a particular design across a wide range of browsers, your only choice is to use JavaScript to create a custom component. </p> | |
<p>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 <code>select</code> 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 <strong>every</strong> 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 <em>menu</em> required complex custom styling such as thumbnails or complex data formatting for the options.</p> | |
<p>More recently, we've been using a lightweight approach that we open sourced as our <a href="https://github.com/filamentgroup/select">custom select</a> widget. This works by placing a native <code>select</code> 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 <code>option</code>. It's not much code, but it adds a depedency on JavaScript we need to think about.</p> | |
<p>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 <code>select</code>. 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.</p> | |
<p><strong><a href="http://jsbin.com/yaruh">View demo</a></strong> (in JSBin)</p> | |
<h2>Browser Test Results</h2> | |
<p>Note that all these are screenshots from <a href="http://www.browserstack.com/">Browserstack</a> but we also tested thoroughly on real devices in our lab to ensure the menus worked and focus styles were clear on each device.</p> | |
<!-- This is just temporary for review, upload to our server. Source file in Dropbox/clients/_goodies/ --> | |
<a href="http://s22.postimg.org/lvobf2r3k/css_selects_testing_results.jpg" target='_blank'><img src="http://s22.postimg.org/j1l61moxd/css_selects_testing_results.jpg" border="0"" alt="Screenshots of the custom CSS select in al popular browsers" style="width:100%"/></a> | |
<h2>The technique</h2> | |
<p>In a nutshell: this technique works by removing all the native <code>select</code> element's appearance except for the text that shows the currently selected option. We then wrap the <code>select</code> 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 <code>select</code> elements. Lastly, a pseudo element adds the right hand arrow to finish up our custom select.</p> | |
<p>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 <code>select</code> styling.</p> | |
<p>With our progressive enhancement mindset, the overall goal is to be able to style <code>select</code> elements consistently in as many browsers as possible, but also ensure that the technique crisply degrades to a simple native <code>select</code> in less capable browers so we don't break the core functionality with our newfangled CSS.</p> | |
<p>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. </p> | |
<p>If you want to just copy and paste this code and start styling, <a href="http://jsbin.com/yaruh/">view the code and demo (JSBin)</a>. If you're interested in the CSS hackery involved, read on for the gory details.</p> | |
<h2>How it works</h2> | |
<p>The markup for our custom styled select starts with a <code>select</code> wrapped in a container with the class of <code>custom-select</code>. As with any form element, always include a <code>label</code> for accessibility, either by wrapping <code>label</code> around the <code>select</code>, or stacking and associating the label with a <code>for</code> attribute that matches the ID of the <code>select</code> as shown.</p> | |
<pre><code> | |
<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> | |
</code></pre> | |
<p>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 <code>-webkit</code> prefixed and standard <a href="http://css-tricks.com/almanac/properties/a/appearance/"><code>appearance: none</code></a> 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 <code>none</code> to align the text in the custom select.</p> | |
<pre> | |
.custom-select select { | |
width:100%; | |
margin:0; | |
background:none; | |
border: 1px solid transparent; | |
outline: none; | |
<strong>/* Prefixed box-sizing rules necessary for older browsers */</strong> | |
-webkit-box-sizing: border-box; | |
-moz-box-sizing: border-box; | |
box-sizing: border-box; | |
<strong>/* Remove select styling */</strong> | |
appearance: none; | |
-webkit-appearance: none; | |
<strong>/* Magic font size number to prevent iOS text zoom */</strong> | |
font-size:16px; | |
<strong>/* General select styles: change as needed */</strong> | |
font-family: helvetica, sans-serif; | |
font-weight: bold; | |
color: #444; | |
padding: .6em 1.9em .5em .8em; | |
line-height:1.3; | |
} | |
</pre> | |
<p>Next, we add the styles for the dropdown wrapper which are very simple. The <code>position: relative;</code> rule creates a positioning context so we can absolutely position the custom arrow image. In this example, the <code>button</code> class on this wrapper has all the custom button styles that you will customize: gradient, shadow, border, text color, etc. </p> | |
<pre> | |
.custom-select { | |
position: relative; | |
display:block; | |
margin-top:0.5em; | |
padding:0; | |
} | |
</pre> | |
<p>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.</p> | |
<p> Note that this arrow element sits on top of the native <code>select</code> because of the <code>z-index:2</code> rule so tapping the arrow will prevent the menu from opening and might be confusing to our visitors. To address this, we add a <a href="http://css-tricks.com/almanac/properties/p/pointer-events/"><code>pointer-events:none;</code></a> rule to make a tap on the arrow open the <code>select</code> even though it is beneath the arrow. Note that since the <code>pointer-events</code> trick won't work everywhere, keeping the arrow image small relative to the select will minimize this drawback.</p> | |
<pre> | |
.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; | |
} | |
</pre> | |
<p>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.</p> | |
<h2>Internet Explorer</h2> | |
<p>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 <a href="http://browserhacks.com/#hack-f1070533535a12744a0381a75087a915">clever CSS hack</a> that gives us the ability target the proprietary IE <code>select::-ms-expand</code> rule to hide the native select styling in IE 10+. </p> | |
<p>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 <a href="http://stackoverflow.com/questions/17553300/change-ie-background-color-on-unopened-focused-select-box"><code>select:focus::-ms-value</code></a> property. </p> | |
<p>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.</p> | |
<pre> | |
<strong>/* IE 10/11+ - hide native select arrow in favor of custom select styles */</strong> | |
_:-ms-input-placeholder, :root .custom-select select::-ms-expand { | |
display: none; | |
} | |
<strong>/* Removes the odd blue bg color behind the text in the select button in IE 10/11 */</strong> | |
_:-ms-input-placeholder, :root .custom-select select:focus::-ms-value { | |
background: transparent; | |
color: #222; | |
} | |
</pre> | |
<h2>Opera</h2> | |
<p>Opera's Presto browser never supported any form of <code>appearance: none</code> 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. <a href="http://browserhacks.com/#hack-a3f166304aafed524566bc6814e1d5c7">This hack</a> works on Opera 9 and above, older versions will have a double arrow.</p> | |
<pre> | |
<strong>/* Opera - Pre-Blink nix the custom arrow, go with a native select button */</strong> | |
x:-o-prefocus, .dropdown::after { | |
display:none; | |
} | |
</pre> | |
<p>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.</p> | |
<h2>Firefox</h2> | |
<p>Firefox has a long, complex relationship with <code>appearance: none</code>. Firefox shipped with the prefixed <code>-moz-appearance</code> property in version 5. However, later versions of Firefox broke support for <code>-moz-appearance: none;</code> and warned that support was <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance">coming to an end</a>. Undeterred, folks found <a href="https://gist.github.com/joaocunha/6273016">clever workarounds</a> that used a combination of <code>text-indent</code> and <code>text-overflow</code> to nudge the native select's arrow just a bit to hide it from view. By wrapping those rules in a <a href="http://browserhacks.com/#fx">CSS hack</a>, it's possible to target this ugliness only to certain versions of Firefox:</p> | |
<pre> | |
@-moz-document url-prefix() { | |
select { | |
-moz-appearance: none; | |
text-indent: 0.01px; | |
text-overflow: ""; | |
} | |
} | |
</pre> | |
<p>That worked for a while, but Firefox eventually stopped respecting <code>-moz-appearance: none;</code> in later versions so the native arrow started popping up again. Undeterred, someone figured out that using the very hacky <a href="http://stackoverflow.com/questions/5912791/how-to-remove-the-arrow-from-a-select-tag-in-firefox"><code>-moz-appearance: window</code></a> would again reliably hide the select appearance in Firefox. The downside with using <code>window</code> 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.</p> | |
<p>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 <a href="http://www.currelis.com/hiding-select-arrow-firefox-30.html">two possible workarounds</a>: either set the <code>select</code> width to 110% and clip the arrow with <code>overflow: hidden</code> 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.</p> | |
<p>So where did we land? Well, in Firefox 5 and earlier, we go with a native select. All we need to do is <a href="http://browserhacks.com/#hack-756739b33c37fb7db4cce645bbf7ea5f">target a hack to old Firefox</a> to hide the custom arrow and remove the right padding on the select to make thigs look correct.</p> | |
<pre> | |
<strong>/* Firefox >= 2 */ | |
/* Show only the native arrow */</strong> | |
body:last-child .dropdown::after, x:-moz-any-link { | |
display: none; | |
} | |
<strong>/* reduce padding */</strong> | |
body:last-child .dropdown select, x:-moz-any-link { | |
padding-right: .8em; | |
} | |
</pre> | |
<p>In Firefox 6+, we <a href="http://browserhacks.com/#hack-443a4eb168ccf066e7c2909be4911af1">target</a> styles that override the older Firefox rules to upgrade the <code>select</code> 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. </p> | |
<pre> | |
<strong>/* Show the custom arrow again */</strong> | |
_::-moz-progress-bar, body:last-child .custom-select:after { | |
display: block; | |
} | |
<strong>/* Hide the native select appearance */</strong> | |
_::-moz-progress-bar, body:last-child .custom-select select { | |
-moz-appearance: window; | |
text-indent: 0.01px; | |
text-overflow: ""; | |
<strong>/* increase padding to make room for menu icon */</strong> | |
padding-right: 13%; | |
} | |
</pre> | |
<p>Finally, we have no other choice but to use clipping to make Firefox 30+ behave but we <a href="http://browserhacks.com/#hack-cfd4c21603b122acfda8e81f41cdb320">target it</a> 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.</p> | |
<p>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.</p> | |
<pre> | |
@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal) { | |
<strong>/* Clip select with the container */</strong> | |
_::-moz-progress-bar, body:last-child .custom-select { | |
overflow: hidden; | |
} | |
<strong>/* Make select extra wide so it clips off */</strong> | |
_::-moz-progress-bar, body:last-child .custom-select select { | |
width: 110%; | |
} | |
} | |
</pre> | |
<p>We also found that Firefox applies a strange dotted outline around the text of the select which can be negated by this rule.</p> | |
<pre> | |
/* Firefox focus has odd artifacts around the text, this fixes that */ | |
select:-moz-focusring { | |
color: transparent; | |
text-shadow: 0 0 0 #000; | |
} | |
</pre> | |
<p>If all this CSS "finesse makes you uncomfortable, you can choose to omit these Firefox rules and stick with a native select. For those determined to make custom styles work, at least there is a solid (albeit hacky) way to do this. | |
<h2>Wrapping up</h2> | |
<p>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 <code>select</code> 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.</p> | |
<p>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 <a href="http://www.w3.org/TR/2014/WD-shadow-dom-20140617/">shadow DOM</a> that give us a more standards-friendly way to style elements.</p> | |
<script id="jsbin-javascript"> | |
// Fork from http://jsbin.com/cihor/11/edit | |
</script> | |
<script id="jsbin-source-javascript" type="text/javascript">// Fork from http://jsbin.com/cihor/11/edit</script></body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fork from http://jsbin.com/cihor/11/edit |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment