<select>
dropdown with CSSEditor’s note: This article was last updated by Shalitha Suranga on 25 June 2024 to include details about cross-browser compatibility, responsiveness, and dynamic styling with CSS variables and to update code blocks.
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 tutorial, we will explore how to achieve a consistent appearance while utilizing the native <select>
element using pure CSS. By doing so, we can preserve all the accessibility advantages that come 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. We’ll create two select widgets. You can check out the finished demos on CodePen — one created using only CSS, and the other created with CSS and JavaScript.
<select>
element worksTo 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:
<select> <option>Open this select menu</option> <option>GitHub</option> <option>Instagram</option> <option>Facebook</option> <option>LinkedIn</option> <option>Twitter</option> <option>Reddit</option> </select>
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 our guide to creating a custom range slider, 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 isolation comes with certain limitations in terms of what we can style.
The dropdown menu UI of the select element is not a part of the DOM and it’s rendered as a native UI element that’s not stylable with CSS (limited CSS properties might work in some browsers), so its implementation is hidden in a shadow DOM node:
However, the select element box is a DOM element that is styled with the user agent stylesheet in all modern standard browsers, so we can customize it with pure CSS.
<select>
dropdown with only CSSCreate a new select dropdown by wrapping the previous select markup with a div
element as follows:
<div class="custom-select"> <select> <option>Open this select menu</option> <option>GitHub</option> <option>Instagram</option> <option>Facebook</option> <option>LinkedIn</option> <option>Twitter</option> <option>Reddit</option> </select> </div>
We can add the following CSS to provide some basic styling:
.custom-select { min-width: 350px; } .custom-select select { appearance: none; 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>
dropdownBy utilizing CSS pseudo-elements, we can create a custom arrow without adding an 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
— inside the container with absolute positioning:
.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 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 selected 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:
See the Pen
Custom-select-dropdown-CSS by Shalitha Suranga (@shalithasuranga)
on CodePen.
There are a few things 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:
.custom-select select { /* ... */ background-color: #000; color: #fff; } .custom-select::before { /* ... */ border-bottom: var(--size) solid #fff; } .custom-select::after { /* ... */ border-top: var(--size) solid #fff; }
The above code snippet renders a black background for the initial select box. It also makes the native dropdown menu black on Chrome (Firefox renders a gray color background):
It’s also possible to style your custom dropdowns based on the system color scheme with CSS media queries and CSS variables, as shown in the following code snippet:
:root { /* light theme */ --dropdown-background: #fff; --dropdown-text-color: #000; --dropdown-hover-color: #eee; --dropdown-border-color: #caced1; --dropdown-border-radius: 0.25em; --dropdown-icon-color: #000; --background-color: #eee; --text-color: #000; } body { /* ... */ background-color: var(--background-color); color: var(--text-color); } .custom-select select { /* ... */ background-color: var(--dropdown-background); border-radius: var(--dropdown-border-radius); color: var(--dropdown-text-color); border: 1px solid var(--dropdown-border-color); } .custom-select select:hover { background-color: var(--dropdown-hover-color); } .custom-select::before { /* ... */ border-bottom: var(--size) solid var(--dropdown-icon-color) } .custom-select::after { /* ... */ border-top: var(--size) solid var(--dropdown-icon-color); } /* dark theme */ @media (prefers-color-scheme: dark) { :root { --dropdown-background: #000; --dropdown-text-color: #fff; --dropdown-hover-color: #222; --dropdown-border-color: #555; --dropdown-border-radius: 0.25em; --dropdown-icon-color: #fff; --background-color: #000; --text-color: #fff; } }
Similarly, you can build various themes for your web app by using different values for --dropdown-background
-like CSS variables we used in the above example.
Again, custom styles are limited to the initial appearance of the dropdown. We can’t customize the opened list of options, add additional elements like images, or achieve a consistent background color for each option.
Implementing consistent customization for native select elements with CSS is limited due to browser limitations, but it’s possible to implement customizations for the initial select box and basic color customizations for the native dropdown list. For example, we can change the native select dropdown background and text color using color
and background-color
on the <select>
element on most browsers.
Some browsers may support additional properties and some won’t. For example, you can use font-style
with <select>
to set styles for the native dropdown list items on Chrome, but that property won’t work on Firefox:
So, we can’t expect better cross-browser compatibility for styling the native dropdown list with CSS, but we can style the DOM-based initial dropdown select box using the appearance
property as we demonstrated above.
Responsiveness is another factor to consider in modern apps. The dropdown selector box is a DOM element, so we can set a dynamic width for it, and the browser automatically handles the responsiveness of the native dropdown list for us as follows:
.custom-select { /* ... */ width: 100%; }
In the next section, we’ll explore how to create a fully custom <select>
dropdown from the ground up.
<select>
dropdown from scratch with CSS and JavaScriptThe 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, so we will be able to customize every atomic segment with CSS, unlike the native select element.
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 used 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!
Here is 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 tabs from the select button into the list box. We’ll hide the radio buttons later to prevent the dropdown from becoming too visually cluttered.
In the next step, we’ll add appropriate ARIA attributes so the custom <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-label="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 of the button
element:
role="combobox"
identifies the button as the element that controls the list boxaria-label
describes the button’s purposearia-haspopup
informs the user’s screen reader that there is an interactive popup element and describes its typearia-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 elementaria-expanded
toggles between true
and false
values to indicate the state of the dropdown — in other words, whether the dropdown is currently hidden or visible. Later, we’ll use JavaScript to dynamically update the value to reflect the current state of the dropdownNext, on the ul
element:
role="listbox"
identifies the ul
as a list from which a user may select an itemrole="option"
represents an individual selectable option for each child in the listNow, let’s move on to styling our custom <select>
dropdown.
<select>
dropdownWe’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; } .custom-select .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; } .custom-select .selected-value { text-align: left; } .custom-select .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 custom <select>
dropdown.
One important detail here is position:
relative
, which we applied to the containing element. This will let us place the dropdown below the button with absolute positioning. When the user opens the dropdown, it will overlap other content on the page.
Notice that 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); box-sizing: border-box; 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 below preview, it improves the user experience by visually highlighting the part of the dropdown widget receiving focus:
The preview above demonstrates how the user can navigate the dropdown using their keyboard’s tab and arrow keys.
hover
, checked
, and focus
statesFor 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 custom <select>
dropdown:
.select-dropdown li:hover, .select-dropdown input:checked ~ label { background-color: #f2f2f2; } .select-dropdown input:focus ~ label { background-color: #dfdfdf; }
Here, we used the ~
syntax, or the subsequent-sibling combinator syntax, to select the label
element when the related checkbox is checked or focused. The following preview demonstrates how the select widget behaves with the custom styling we just added:
Now that we can visualize how the radio input works, we can hide it with the following CSS rules:
.select-dropdown input[type="radio"] { position: absolute; left: 0; opacity: 0; }
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.
You can check how the above JavaScript code snippet dynamically updates the DOM with dev tools as follows:
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); });
Once you add the above JavaScript after the previous click event listener code, you can select a list item by clicking on it or pressing the return key, as demonstrated in the following preview:
<select>
optionsLet’s add icons alongside the options using Boxicons. Start by adding the Boxicons CDN to the <head>
element:
<head> <!-- ... --> <link href="https://unpkg.com/[email protected]/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 below:
See the Pen
Custom-select-dropdown-CSS+JS by Shalitha Suranga (@shalithasuranga)
on CodePen.
Similar to icons, you can add any secondary item to each list item using HTML and CSS. Creating a custom <select>
dropdown using this approach ensures accessibility and also offers more flexible customization than the native <select>
element.
We built the above custom <select>
dropdown using HTML, CSS, and JavaScript without using the native <select>
element, so the dropdown looks the same on any modern standard browser. If you write standard CSS that is compatible with modern browsers, your custom dropdown widget won’t face cross-browser compatibility issues.
The custom <select>
dropdown we built is responsive even though we used 400px
for demonstration. Try setting the 100%
relative value as follows to make it responsive:
.custom-select { /* ... */ width: 100%; }
Now, you can use the above class with any CSS library or your own layout grid system. For example, you can use it with Bootstrap:
<div class="container"> <div class="row"> <div class="col-lg-4 col-md-6 col-sm-12"> <div class="custom-select"> <!-- ... --> </div> </div> </div> </div>
The above layout renders dynamic sizes for different viewports, as shown in the following preview:
The native <select>
dropdown, like other native HTML form elements, can be challenging to style because it renders a native dropdown list that is not part of the standard DOM. In this tutorial, 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 and allowing for a more consistent and visually appealing component. Customizing the initial style of the native select dropdown is undoubtedly simple and fast, but its customizations are limited because we can’t style <option>
elements, so it is a good styling method for minimal customization requirements.
In the second approach, we built a custom dropdown from scratch with HTML, CSS, and JavaScript. We created a complete custom select dropdown using semantic elements for accessibility and keyboard interactivity. This approach offers the maximum flexibility and customizability because we used standard DOM elements, unlike the native select dropdown, so this is a suitable method for creating advanced dropdowns that render secondary items within list items.
If you found this tutorial helpful, please share it with others. Your feedback and contributions are welcome in the comment section.
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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowIn this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
SOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
2 Replies to "Creating a custom <code><select></code> dropdown with CSS"
Hi, thank You for this article! What happens if I have to create many custom selects on the same page? How do you suggest modifying the code?
Hi, thanks for your article! Obviously the code doesn’t work if We have many custom selects on the same page. How would You change the code in this case?