on
The (almost) impossible task of styling <select> in CSS
Even at 5+ years of fullstack development, I find CSS to be my Achilles’ heel. Recently, I ran into the need to alter how basic input elements like <input>
, <select>
, etc., look, and while it was simple to make others obey, the <select>
was one tricky customer. No matter how much I tried, it completely ignored any changes in rounded corners: the corners were always rounded equally, and that too by the default radius.
Turns out there’s absolutely nothing you can do about it. I learned this when I was researching this weird thing and landed on one of the GitHub issues from the popular Bootstrap project. In short: that’s how Webkit-based browsers behave, so deal with it. 🖕
That said, it’s not as if you can’t style a <select>
. Not if you’re willing to jump through many hoops. So, let’s learn how to achieve that step by step.
We naturally need to employ some trick, and the trick here is to wrap the <select>
in a surrounding <div>
and style that <div>
instead. And then “neutralize” the default styling of the <select>
.
To be very specific:
- Wrap our
<select>
with another div and perform most of the desired styling there. - Remove all borders, etc., from the
<select>
so that the background color from the parent is seamless. - Make the
<select>
a block element so its effective “area” is the same as its parent’s. - The default arrows will mess with the design, so hide them with
-webkit-appearance: none
. - Supply our own arrow signs and place them with
position: absolute
.
There are many other minor tricks involved, all explained as comments in the CSS (with HTML) snippet below.
<div class="select-wrapper">
<select>
<option value="car">Car</option>
<option value="bike">Bike</option>
<option value="bus">Bus</option>
</select>
</div>
.select-wrapper {
width: 100px; /* Not important here */
position: relative; /* for positioning the child */
border-radius: 0 1em 2em 0;
background-color: #444;
color: #fff;
border: none;
padding: 15px 20px;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
position: absolute; /* We must place the `select` "within" the parent */
top: 0; left: 0;
padding-left: 10px; /* align the text inside */
background: transparent; /* Without this, `select` will not adopt the parent's background color */
color: inherit; /* inherit instead of aplpying browser default of black */
width: 100%; height: 100%;
border: none; /* remove default outline */
outline: none; /* Do not draw borders around when interacting with it - another annoying browser default*/
}
Our select
already looks pretty rad. The only thing missing is an arrow, indicating visually that this is a dropdown:
Here’s the HTML again, with the arrow added in:
<div class="select-wrapper">
<select>
<option value="car">Car</option>
<option value="bike">Bike</option>
<option value="bus">Bus</option>
</select>
<span class="arrow">▼</span>
</div>
And we position it a little:
.arrow {
position: absolute;
right: 1em;
top: 0.75em;
font-size: 0.75em;
}
Which results in:
But guess what, while now our dropdown looks perfect, it works no more!
That’s because the arrow we just added is eating up the mouse clicks/taps on the dropdown and these are not getting passed to the actual select
element in the markup. The fix is simple; add this line to the .arrow
CSS:
pointer-events: none;
In case you want to fiddle with the code live, here’s the CodePen link.
A lot of hassle, if you ask me. But once you understand the underlying ideas and practice a little, that dream UI isn’t out of reach!