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:

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:

A styled dropdown No arrows, but it works

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:

Looks amazing!

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!