Styling input elements in CSS – the slippery `select`

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 a lot of 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 instead. And then “neutralize” the default styling of the <select>. To be very specific:

  1. Wrap our <select> with another div and perform most of the desired styling there.
  2. Remove all borders, etc., from the <select> so that the background color from the parent is seamless.
  3. Make the <select> a block element so its effective “area” is the same as its parent’s.
  4. The default arrows will mess with the design, so hide them with -webkit-appearance: none.
  5. Supply our own arrow signs and place them with position: absolute.

There are many other minor tricks involved, all explain as comments in the CSS code 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 the position it a little:

.arrow {
  position: absolute;
  right: 1em;
  top: 0.75em;
  font-size: 0.75em;
}

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!