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:
- Understanding how the
<select>
element works - Customizing the
<select>
dropdown with CSS only - Creating a custom
<select>
dropdown from scratch with CSS and JavaScript
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>
We’ll get a widget that looks like one of the following, with slight differences depending on the user’s 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: 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>
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;
}
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: 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;
}
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%;
}
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:
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;
}
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>
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: 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>
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 thearia-controls
attribute on the controlling element -
aria-expanded
toggles betweentrue
andfalse
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 theul
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;
}
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: 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;
}
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: 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;
}
The GIF below demonstrates how the select widget behaves with the custom styling we just added:
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;
}
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);
}
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"
);
});
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.
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");
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);
});
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>
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>
After that, apply the following CSS for better spacing and alignment:
.select-dropdown li label {
/* ... */
display: flex;
gap: 1rem;
align-items: center;
}
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 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)