DEV Community

Cover image for Creating a custom <select> dropdown with CSS
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Creating a custom <select> dropdown with CSS

Written by Ibadehin Mojeed✏️

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:

We’ll create two select widgets in this tutorial. You can check out the finished demos on CodePen — one created using CSS only, and the other created with CSS and JavaScript.

Understanding how the <select> element works

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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 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.

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 In the custom range slider article, 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

Let's revisit the dropdown markup we mentioned earlier:

<div class="custom-select">
  <select>
    <option value=""> ... </option>
  </select>
</div>
Enter fullscreen mode Exit fullscreen mode

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

.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;
}
Enter fullscreen mode Exit fullscreen mode

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 Let’s create a custom arrow for our <select> dropdown.

Adding a custom arrow to the native <select> dropdown

By utilizing CSS pseudo-elements, 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:

.custom-select {
  /* ... */
  position: relative;
}
Enter fullscreen mode Exit fullscreen mode

Then, we’ll position our CSS pseudo-elements — such as ::before and ::after — absolutely inside the container:

.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%;
}
Enter fullscreen mode Exit fullscreen mode

In the code, we have employed the "border trick" 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

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.

Considerations to keep in mind regarding a 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:

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

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

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

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

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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 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 to prevent the dropdown from becoming too visually cluttered.

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:

<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>
Enter fullscreen mode Exit fullscreen mode

Some of these attributes may look familiar if you read our previous tutorial on making dropdown menus with 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 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

We’ll start with styling the initial appearance — in other words, the select button and its content — with the following 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;
}
Enter fullscreen mode Exit fullscreen mode

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 Next, the following CSS will style the list box:

.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;
}
Enter fullscreen mode Exit fullscreen mode

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 — 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 The GIF above demonstrates how the user can navigate the dropdown using their keyboard.

Adding hover, checked, and 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:

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

.select-dropdown input:focus ~ label {
  background-color: #dfdfdf;
}
Enter fullscreen mode Exit fullscreen mode

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

Hiding the radio buttons

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

.select-dropdown input[type="radio"] {
  position: absolute;
  left: 0;
  opacity: 0;
}
Enter fullscreen mode Exit fullscreen mode

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:

.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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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"
  );
});
Enter fullscreen mode Exit fullscreen mode

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

Displaying the 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:

const selectedValue = document.querySelector(".selected-value");
const optionsList = document.querySelectorAll(".select-dropdown li");
Enter fullscreen mode Exit fullscreen mode

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:

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);
});
Enter fullscreen mode Exit fullscreen mode

Adding secondary information to the <select> options

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

<head>
  <!-- ... -->
  <link
    href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
    rel="stylesheet"
  />
</head>
Enter fullscreen mode Exit fullscreen mode

Then, add the necessary icons alongside the label text:

<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>
Enter fullscreen mode Exit fullscreen mode

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

.select-dropdown li label {
  /* ... */
  display: flex;
  gap: 1rem;
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

See the final result on CodePen.

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.

LogRocket Signup

LogRocket 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.

Top comments (0)