Creating a custom <select> dropdown with CSS
Tags: css, webdev
Published: 2023-08-29 17:36:48

**Written by [Ibadehin Mojeed](https://blog.logrocket.com/author/ibadehinmojeed/)✏️**

A dropdown menu presents a list of options for users to choose from. This widget is commonly used in forms to collect user input. In some cases, it can also be used as a filtering mechanism. 

We typically add dropdowns to a webpage using the native HTML `<select>` element. However, due to the complexity of its internal structure, achieving consistent styling for the native element across different browsers is quite challenging. 

To deploy a fully customized and accessible select component, we have two main options: using a component library or building it from scratch. 

In this lesson, we will explore how to achieve a consistent appearance while still utilizing the native `<select>` element using CSS. By doing so, we can preserve all the accessibility advantages that come built-in with the native element. 

Later on in this article, we will also delve into creating a custom select widget from scratch in the most effective manner using JavaScript. 

Jump ahead:

*   [Understanding how the `<select>` element works](#understanding-select-element)
*   [Customizing the `<select>` dropdown with CSS only](#customizing-select-dropdown-css-only)
    *   [Adding a custom arrow to the native `<select>` dropdown](#adding-custom-arrow-native-select-dropdown)
    *   [Considerations to keep in mind regarding a CSS-only implementation](#considerations-regarding-css-only-implementation)
*   [Creating a custom `<select>` dropdown from scratch with CSS and JavaScript](#custom-select-dropdown-scratch-css-javascript)
    *   [Adding accessibility features](#adding-accessibility-features)
    *   [Styling the `<select>` dropdown](#styling-select-dropdown)
    *   [Adding `hover`, `checked`, and `focus` states](#adding-hover-checked-focus-states)
    *   [Hiding the radio buttons](#hiding-radio-buttons)
    *   [Implementing JavaScript code](#implementing-javascript-code)
    *   [Displaying the selected option](#displaying-selected-option)
    *   [Adding secondary information to the `<select>` options](#adding-secondary-information-select-options)

We’ll create two select widgets in this tutorial. You can check out the finished demos on CodePen — [one created using CSS only](https://codepen.io/ibaslogic/pen/abQQerP), and the [other created with CSS and JavaScript](https://codepen.io/ibaslogic/pen/KKrbGRR).

## Understanding how the `<select>` element works <a name="understanding-select-element">

To understand why styling the `<select>` dropdown is challenging, let's examine its internal mechanisms. 

Using the following HTML markup, we can create a dropdown list with seven selectable options:

```html
<div class="custom-select">
  <select>
    <option value="">Open this select menu</option>
    <option value="">GitHub</option>
    <option value="">Instagram</option>
    <option value="">Facebook</option>
    <option value="">LinkedIn</option>
    <option value="">Twitter</option>
    <option value="">Reddit</option>
  </select>
</div>
```

We’ll get a widget that looks like one of the following, with slight differences depending on the user’s browser: ![Three Simple Select Menus Styled Slightly Differently Based On Browser](http://blog.logrocket.com/wp-content/uploads/2023/08/img1-Simple-select-menus-different-browsers.png) If we take a moment and inspect the widget using the browser’s developer tools, we’ll notice that it is implemented as a web component [inside a shadow DOM](https://blog.logrocket.com/virtual-dom-react/#how-virtual-dom-different-shadow-dom). 

If you’re on Chrome, you can enable the **Show user agent shadow DOM** option in your browser’s settings to see the shadow DOM: ![Chrome Developer Tools With User Agent Shadow Dom Enabled. Red Box Outlines Labeled One And Two Show Shadow Root For User Agent On Select Menu](http://blog.logrocket.com/wp-content/uploads/2023/08/img2-Select-menu-shadow-DOM.png) In [the custom range slider article](https://blog.logrocket.com/creating-custom-css-range-slider-javascript-upgrades/), we discussed how web browsers internally encapsulate and hide elements and styles that constitute the UI control using a shadow DOM. 

One benefit of using the shadow DOM is that it prevents conflicts between the styles of a component and those of other elements in the actual DOM. This results in a more predictable and reliable user interface. 

However, this isolation comes with certain limitations in terms of what we can style. Additionally, because of different implementations across browsers, inconsistencies may arise.

## Customizing the `<select>` dropdown with CSS only <a name="customizing-select-dropdown-css-only">

Let's revisit the dropdown markup we mentioned earlier:

```html
<div class="custom-select">
  <select>
    <option value=""> ... </option>
  </select>
</div>
```

We can add the following CSS to provide some initial styles:

```css
.custom-select {
  min-width: 350px;
}

select {
  appearance: none;
  /* safari */
  -webkit-appearance: none;
  /* other styles for aesthetics */
  width: 100%;
  font-size: 1.15rem;
  padding: 0.675em 6em 0.675em 1em;
  background-color: #fff;
  border: 1px solid #caced1;
  border-radius: 0.25rem;
  color: #000;
  cursor: pointer;
}
```

One important piece of code here is the `appearance: none` declaration. It instructs the browser to remove the default appearance styles of the native dropdown. This is a crucial step we need to take before we apply our custom styles. 

Looking at the result below, you can see that setting the `appearance` property to `none` also removes the native dropdown arrow: ![Select Menu With No Dropdown Arrow Displayed As Widget Labeled Open This Select Menu](http://blog.logrocket.com/wp-content/uploads/2023/08/img3-Select-menu-no-dropdown-arrow.png) Let’s create a custom arrow for our `<select>` dropdown.

### Adding a custom arrow to the native `<select>` dropdown <a name="adding-custom-arrow-native-select-dropdown">

By utilizing [CSS pseudo-elements](https://blog.logrocket.com/css-pseudo-elements-guide/), we can create a custom arrow without the need to add an additional HTML element. To achieve this, we’ll apply a `position: relative` property to the container element:

```css
.custom-select {
  /* ... */
  position: relative;
}
```

Then, we’ll position our CSS pseudo-elements — [such as `::before` and `::after`](https://blog.logrocket.com/css-before-after-custom-animations-transitions/) — absolutely inside the container:

```css
.custom-select::before,
.custom-select::after {
  --size: 0.3rem;
  position: absolute;
  content: "";
  right: 1rem;
  pointer-events: none;
}

.custom-select::before {
  border-left: var(--size) solid transparent;
  border-right: var(--size) solid transparent;
  border-bottom: var(--size) solid black;
  top: 40%;
}

.custom-select::after {
  border-left: var(--size) solid transparent;
  border-right: var(--size) solid transparent;
  border-top: var(--size) solid black;
  top: 55%;
}
```

In the code, we have employed the "[border trick](https://blog.logrocket.com/creating-triangles-using-css/)" to create both the up and down arrow indicators. By utilizing CSS borders in this way, we have designed arrow shapes that suit the desired appearance. 

Additionally, we have disallowed pointer events on the arrow element to enforce proper functionality. This way, when users click on the arrow, the select element receives the click event, not the arrow itself. 

Below is the behavior without the CSS `pointer-events: none;` declaration: ![Demo Of Behavior Of Select Element Without Point Events None Declaration. User Shown Clicking On Dropdown Button With No Effect, Then Clicking On Element To Open Select Menu](http://blog.logrocket.com/wp-content/uploads/2023/08/img4-Demo-behavior-select-element-without-pointer-events-none-declaration.gif) 

Disallowing pointer events on the arrow element ensures that the dropdown opens as expected when interacting with the arrow indicator, providing a seamless and user-friendly experience. 

See the [result on CodePen](https://codepen.io/ibaslogic/pen/abQQerP).

### Considerations to keep in mind regarding a CSS-only implementation <a name="considerations-regarding-css-only-implementation">

There are a few notes you should consider when customizing the `<select>` dropdown using this method. 

For consistent design across browsers, we adopt a strategic approach by styling the native `<select>` dropdown to closely resemble the browser's default appearance. This ensures the dropdown maintains a familiar and uniform look, giving users a consistent experience across web browsers. 

Additionally, with the native `<select>` element, we have control over the initial appearance of the dropdown — that is, how the select dropdown looks before it is opened. However, once the `<select>` element is opened, the list of options will take the browser's default styling. 

If you want the dropdown to appear black initially, with white arrows and text, you can modify the CSS to the following:

```css
select {
  /* ... */
  background-color: #000;
  color: #fff;
}

.custom-select::before {
  /* ... */
  border-bottom: var(--size) solid #fff;
}

.custom-select::after {
  /* ... */
  border-top: var(--size) solid #fff;
}
```

Again, custom styles are limited to the initial appearance of the dropdown. We cannot customize the opened list of options, add additional elements like images, or achieve a consistent background color for each option. 

In the next section, we’ll explore how to create a fully custom `<select>` dropdown from the ground up.

## Custom `<select>` dropdown from scratch with CSS and JavaScript <a name="custom-select-dropdown-scratch-css-javascript">

The native `<select>` element automatically generates components like the select button and list box. However, in this custom implementation, we'll manually assemble the necessary elements. Additionally, rather than using generic elements, we’ll employ semantic and meaningful elements. 

Take a look at the markup below:

```html
<div class="custom-select">
  <button class="select-button">
    <span class="selected-value">Open this select menu</span>
    <span class="arrow"></span>
  </button>
  <ul class="select-dropdown">
    <li>
      <input type="radio" id="github" name="social-account" />
      <label for="github">GitHub</label>
    </li>
    <li>
      <input type="radio" id="instagram" name="social-account" />
      <label for="instagram">Instagram</label>
    </li>
    <!-- ... -->
  </ul>
</div>
```

We have used a `button` element containing a `span` that will display the selected value and another `span` to style a custom arrow. We then use a group of radio buttons to represent the list of options. 

This native element provides the functionality for keyboard interactivity. Remember, we must keep accessibility in mind! 

The result: ![Select Dropdown Creating Using Button Element Containing Span With Group Of Radio Buttons To Represent List Of Options](http://blog.logrocket.com/wp-content/uploads/2023/08/img5-Select-menu-radio-buttons.png) Using radio buttons in the `custom-select` dropdown allows for smooth keyboard navigation using the `Arrow Up` and `Arrow Down` keys after a user tab from the select button into the list box. We’ll [hide the radio buttons later](#hiding-radio-buttons) to prevent the dropdown from becoming too visually cluttered.

### Adding accessibility features <a name="adding-accessibility-features">

In the next step, we’ll add appropriate ARIA attributes so that the `<select>` dropdown can be more accessible for people with disabilities. 

Below are the necessary attributes for our widget:

```html
<div class="custom-select">
  <button
    class="select-button"
    role="combobox"
    aria-labelledby="select button"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-controls="select-dropdown"
  >
    <!-- ... -->
  </button>
  <ul ... role="listbox" id="select-dropdown">
    <li role="option">...</li>
    <li role="option">...</li>
  </ul>
</div>
```

Some of these attributes may look familiar if you read our previous tutorial on [making dropdown menus with CSS](https://blog.logrocket.com/making-dropdown-menus-css/). Let’s define each of them briefly, starting with the attributes on the `button` element:

*   `role="combobox"` identifies the button as the element that controls the list box
*   `aria-labelledby` describes the button’s purpose
*   `aria-haspopup` informs the user’s screen reader that there is an interactive popup element and describes its type
*   `aria-controls` links the controlling element to the expanded widget. In this case, we assigned the ID of the expanded widget to the `aria-controls` attribute on the controlling element
*   `aria-expanded` toggles between `true` and `false` values to indicate the state of the dropdown — in other words, whether the `<select>` dropdown is currently hidden or visible. Later, we’ll [use JavaScript to dynamically update the value](#displaying-selected-option) to reflect the current state of the dropdown

Next, on the `ul` element:

*   `role="listbox"` identifies the `ul` as a list from which a user may select an item
*   `role="option"` represents an individual selectable option for each child in the list

Now, let’s move on to styling our `<select>` dropdown.

### Styling the `<select>` dropdown <a name="styling-select-dropdown">

We’ll start with styling the initial appearance — in other words, the select button and its content — with the following CSS:

```css
.custom-select {
  position: relative;
  width: 400px;
  max-width: 100%;
  font-size: 1.15rem;
  color: #000;
  margin-top: 3rem;
}

.select-button {
  width: 100%;
  font-size: 1.15rem;
  background-color: #fff;
  padding: 0.675em 1em;
  border: 1px solid #caced1;
  border-radius: 0.25rem;
  cursor: pointer;

  display: flex;
  justify-content: space-between;
  align-items: center;
}

.selected-value {
  text-align: left;
}

.arrow {
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid #000;
  transition: transform ease-in-out 0.3s;
}
```

In the code above, we implemented various CSS styles to enhance the overall aesthetics of our `<select>` dropdown. 

One important detail here is the `position:` `relative` that we applied to the containing element. This will let us place the dropdown absolutely below the button. When the user opens the dropdown, it will overlap other content on the page. 

Notice we added a CSS `transition` property on the arrow for a smooth transition effect. We’ll see the effect when we implement interactivity. For now, the appearance should resemble the following: ![Styled Select Menu With Custom Button Styling](http://blog.logrocket.com/wp-content/uploads/2023/08/img6-Styled-select-menu-radio-buttons.png) Next, the following CSS will style the list box:

```css
.select-dropdown {
  position: absolute;
  list-style: none;
  width: 100%;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
  background-color: #fff;
  border: 1px solid #caced1;
  border-radius: 4px;
  padding: 10px;
  margin-top: 10px;
  max-height: 200px;
  overflow-y: auto;
  transition: 0.5s ease;
}

.select-dropdown:focus-within {
  box-shadow: 0 10px 25px rgba(94, 108, 233, 0.6);
}

.select-dropdown li {
  position: relative;
  cursor: pointer;
  display: flex;
  gap: 1rem;
  align-items: center;
}

.select-dropdown li label {
  width: 100%;
  padding: 8px 10px;
  cursor: pointer;
}

.select-dropdown::-webkit-scrollbar {
  width: 7px;
}
.select-dropdown::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 25px;
}

.select-dropdown::-webkit-scrollbar-thumb {
  background: #ccc;
  border-radius: 25px;
}
```

We’ve used a couple of different CSS properties, but let’s talk about the `:focus-within` pseudo-class that we applied to the select dropdown. 

This pseudo-class will apply its rules — in this case, [a `box-shadow` effect](https://blog.logrocket.com/three-ways-style-css-box-shadow-effects/) — when any of its child elements receive focus. As we can see in the GIF below, it improves the user experience by visually highlighting the part of the dropdown widget receiving focus: ![Styled Select Menu With Focus Within Pseudo Class To Apply A Box Shadow Effect To Part Of Dropdown Widget Receiving Focus](http://blog.logrocket.com/wp-content/uploads/2023/08/img7-Styled-select-menu-focus-within-pseudo-class-apply-box-shadow-effect.gif) The GIF above demonstrates how the user can navigate the dropdown using their keyboard.

### Adding `hover`, `checked`, and `focus` states <a name="adding-hover-checked-focus-states">

For the sake of accessibility and a smooth UX, we’ll add styles for `focus`, `hover`, and `active` states to provide a visual effect while interacting with the `<select>` dropdown:

```css
.select-dropdown li:hover,
.select-dropdown input:checked ~ label {
  background-color: #f2f2f2;
}

.select-dropdown input:focus ~ label {
  background-color: #dfdfdf;
}
```

The GIF below demonstrates how the select widget behaves with the custom styling we just added: ![Select Menu With Added Styled For Focus, Hover, And Active States To Provide Visual Effect For Relevant Interactions With Dropdown](http://blog.logrocket.com/wp-content/uploads/2023/08/img8-Select-menu-styles-focus-hover-active-states.gif)

### Hiding the radio buttons <a name="hiding-radio-buttons">

Now that we can visualize how the radio input works, we can now hide it with the following CSS rules:

```css
.select-dropdown input[type="radio"] {
  position: absolute;
  left: 0;
  opacity: 0;
}
```

### Implementing JavaScript code <a name="implementing-javascript-code">

We’ll use JavaScript to toggle the dropdown and select an option from the list box. Let’s begin by making modifications to the CSS:

```css
.select-dropdown {
  /* ... */

  transform: scaleY(0);
  opacity: 0;
  visibility: hidden;
}

/* .... */

/* interactivity */
.custom-select.active .arrow {
  transform: rotate(180deg);
}

.custom-select.active .select-dropdown {
  opacity: 1;
  visibility: visible;
  transform: scaleY(1);
}
```

We’ve hidden the dropdown by default and applied style rules to transform the arrow indicator and display the dropdown when an `.active` class is applied to the container element. We’ll add the `.active` class using JavaScript:

```javascript
const customSelect = document.querySelector(".custom-select");
const selectBtn = document.querySelector(".select-button");

// add a click event to select button
selectBtn.addEventListener("click", () => {
  // add/remove active class on the container element
  customSelect.classList.toggle("active");
  // update the aria-expanded attribute based on the current state
  selectBtn.setAttribute(
    "aria-expanded",
    selectBtn.getAttribute("aria-expanded") === "true" ? "false" : "true"
  );
});
```

The JavaScript code above begins by getting a reference to the select button and the container element. Then, it listens for a click event on the button and dynamically toggles the `.active` class on the container element. 

It also dynamically updates the `aria-expanded` attribute on the actual button based on the current state. 

The result: ![User Shown Interacting With Final Select Menu With Custom Styling And Radio Buttons Removed. Clicking On Select Menu Opens Dropdown While Hovering Over Menu List Highlights It With Box Shadow Effect. Hovering Over List Items Highlights Them Light Grey While Selecting One Highlights It Darker Grey](http://blog.logrocket.com/wp-content/uploads/2023/08/img9-Styled-select-menu-implemented-JavaScript-open-menu-select-option.gif)

### Displaying the selected option <a name="displaying-selected-option">

Once the user selects an option from the list, we want to automatically close the dropdown and display the selected option. To do so, we’ll start by targeting all the options in the list along with the element that should display the value of the currently selected option:

```javascript
const selectedValue = document.querySelector(".selected-value");
const optionsList = document.querySelectorAll(".select-dropdown li");
```

Then, we will loop through each of the options and listen for the user’s selected value. The following code listens for both click and keyboard events:

```javascript
optionsList.forEach((option) => {
  function handler(e) {
    // Click Events
    if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
      selectedValue.textContent = this.children[1].textContent;
      customSelect.classList.remove("active");
    }
    // Key Events
    if (e.key === "Enter") {
      selectedValue.textContent = this.textContent;
      customSelect.classList.remove("active");
    }
  }

  option.addEventListener("keyup", handler);
  option.addEventListener("click", handler);
});
```

### Adding secondary information to the `<select>` options <a name="adding-secondary-information-select-options">

Let’s add icons alongside the options using Boxicons. Start by adding the Boxicons CDN to the `<head>` element:

```html
<head>
  <!-- ... -->
  <link
    href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
    rel="stylesheet"
  />
</head>
```

Then, add the necessary icons alongside the label text:

```html
<ul class="select-dropdown" role="listbox" id="select-dropdown">
  <li role="option">
    <!-- ... -->
    <label for="github"><i class="bx bxl-github"></i>GitHub</label>
  </li>
  <li role="option">
    <!-- ... -->
    <label for="instagram"><i class="bx bxl-instagram"></i>Instagram</label
    >
  </li>
  <!-- ... -->
</ul>
```

After that, apply the following CSS for better spacing and alignment:

```css
.select-dropdown li label {
  /* ... */
  display: flex;
  gap: 1rem;
  align-items: center;
}
```

See the final [result on CodePen](https://codepen.io/ibaslogic/pen/KKrbGRR
).

## Conclusion

The `<select>` dropdown, like other native HTML elements, can be challenging to style. In this lesson, we learned two approaches. 

The first approach used CSS to customize the native `<select>` element. We achieved a custom style by carefully styling the initial appearance allowing for a more consistent and visually appealing component. 

In the second approach, we built a custom dropdown from scratch with CSS and JavaScript. We created a fully custom select dropdown using semantic elements for accessibility and keyboard interactivity. 

If you found this lesson helpful, please share it with others. Your feedback and contributions are welcome in the comment section.

---

## Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, [try LogRocket](https://lp.logrocket.com/blg/css-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2019/12/cpu-memory-usage.png)](https://lp.logrocket.com/blg/css-signup)

[LogRocket](https://lp.logrocket.com/blg/css-signup) is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — [Start monitoring for free](https://lp.logrocket.com/blg/css-signup).