Styling form controls like the <select>
element has been reported as a top developer pain point for years, and we've been working on a solution. While this work is complex and has taken a long time to get right, we're getting very close to landing this feature. A customizable version of the select element is officially in Stage 2 in the WHATWG, with strong cross-browser interest and a prototype for you to test out from Chrome Canary 130.
Try it out and give us your feedback
Check that your install of Chrome Canary is updated to version 130, and that you have the experimental web platform features flag on. You can turn this flag on by going to chrome://flags in your address bar and turning on #experimental-web-platform-features. Then, you should be able to see the Codepen demos in this post. Alternatively, you can check out this Codepen collection to view them all in one place.
Use this form to provide feedback on the feature. It will only take three minutes to complete!
Let's dive into the features of the customizable select API, which builds on the existing HTML select tag.
Opting-in to the new <select>
To opt-in to the new behavior use the CSS appearance
property on both the in-page select button and also on the select picker. To opt-in, set appearance: base-select
on your <select>
element and on the ::picker(select)
.
::picker(select)
is a new user-agent provided pseudo-element that only applies to <select>
elements which have been opted into the new behavior using appearance: base-select
. This picker pseudo-element is the popover that is triggered by the base select button. You can opt-in both as shown in the following code:
select,
::picker(select) {
appearance: base-select;
}
You can choose to only opt-in the in-page button, but you can't opt-in only the picker popover without opting in the in-page button. ::picker(select)
is only created once appearance: base-select
is applied to the <select>
.
Now you're ready to customize your select element. The new customizable select comes with some default styles that look the same across browsers and operating systems. Here's what the default customized select looks like against the existing select in Chrome on macOS:
Breaking down the parts
Once you're in the new customizable select mode, the new elements you now have access to include:
- selectedoption
: reflects the inner HTML of the option which is currently selected.
- option::before
: contains a checkmark to indicate the currently selected option as a default accessibility affordance (this is subject to change).
- ::picker(select)
: popover that contains all of the content outside of the button
inside of a customizable select.
You can style any part of the select. For example, you can add arbitrary non-interactive content within the <option>
elements, style the in-page button which opens the select drop-down, and style the drop-down list of options (the ::picker(select)
).
You can also style the button
, bring-your-own arrow indicator, and add arbitrary content within and surrounding any of the elements. In addition to adding content, you can hide any of these new elements and default styles. For example, if you don't want an indicator checkmark in the ::before pseudo element of the option, use the following CSS.
/* Remove the default checkmark from the selected option */
option::before {
display: none;
}
While there can be an unlimited number of elements inside of your select, the browser will bucket anything outside of a <button>
element into the ::picker(select)
pseudo-element, which behaves as a popover anchored to the button. This <button>
toggles the ::picker(select)
. Options and other elements directly inside of the select will be hoisted into the ::picker(select)
, or you can bring your own wrapper for styling purposes. This wrapper, too, will be placed inside of the ::picker(select)
pseudo-element.
<select>
<button>
<selectedoption></selectedoption>
</button>
// Everything else that will go into the ::picker(select) popover
</select>
The new customizable <select>
uses functionality from popover and anchor positioning. It's built with these two underlying technologies. This means that the drop-down option list within a select acts as a popover which is anchored to the trigger button that opens the select.
You can use anchor positioning to style this ::picker(select)
popover (including making it anchored to other elements). This content model also means that top layer animation styles work with the option list to animate entry and exit effects.
Enhance the existing <select>
element
Previously, the Chrome team was working on the idea of a <selectlist>
element. What's described in this post is that feature redesigned to reuse the existing <select>
element instead.
One of the key benefits of reusing the existing <select>
element is the ability to progressively enhance the basic HTML element. In comparison to a brand new element, reusing <select>
will still render meaningful content on your page. The following example shows the customized select versus what a user in a non-supported browser would see:
Basic styling
Changes may be as simple as visual styling of the select element. For example, to update the button styles, hover and focus styles, or the background of the select options. After opting-in with appearance: base-select
, apply any CSS you want to the parts of your select.
To customize the arrow indicator, add your own button and arrow inside of the select.
<select>
<button>
<selectedoption></selectedoption>
<span>
// Arrow here
</span>
</button>
// Everything else that will go into the ::picker(select) popover
</select>
Then, style the arrow:
/* style the arrow */
button span {
/* arrow styles */
transition: rotate 0.2s;
}
/* adjust arrow styles when the picker is open */
select:open button span {
rotate: -180deg;
}
Complex content within options
Take things further with the ability to add and style content beyond strings within the <option>
elements inside <select>
. A basic example is adding flag images next to country names in a drop-down menu. To achieve this, add an image element next to the option text.
<option value="france">
<img src="img/flag_of_france.svg" alt="" />
<span>France</span>
</option>
A more complex example could include profile photos, names, and alternative information to help you make decisions about which item to select in the drop-down.
<option value="eur">
<img src="euro-flag.png" alt="" />
<div class="currency">
<div class="currency-short">EUR</div>
<div class="currency-long">Euro</div>
</div>
<div class="symbol" aria-hidden="true">€</div>
</option>
Style the selected option
You might want the selected option to be displayed differently in the selected state than it is in the drop-down. An example of this is the gmail UI, where, to save space, the label is removed once the option is selected. Do this by hooking into the <selectedoption>
element for styling. The <option>
contains all of the following markup:
<option value="reply-all">
<img class="material-symbol" src="material-symbol-reply.png">
<span class="text">Reply all</span>
</option>
Now apply display: none
on .text
inside <selectedoption>
' to hide the text content and only show the icon:
selectedoption .text {
display: none;
}
Interactive options
With full control over the styling of the ::picker(select)
, build on the previous demo to make it interactive on hover and focus. In this demo, the new calc-size() function is used to animate the picker's width from showing the icons to showing the full width of the options on hover or if the select has an option with focus-visible.
/* base styles when picker is open but not interacted with */
::picker(select) {
width: var(--icon-width);
transition: width 0.5s;
}
/* animate the text in on hover & focus */
::picker(select):hover,
select:has(option:focus-visible)::picker(select) {
/* auto width! */
width: calc-size(auto, size + 0.5rem);
}
Limitations and accessibility notes
With great power comes great responsibility. To keep things accessible there are some limitations to the feature.
- Other than
<option>
elements, no interactive (focusable) elements are yet allowed inside of the<select>
, such as buttons or other elements. For now, the proposed content model only allows for<div>
,<span>
,<option>
,<optgroup>
,<img>
,<svg>
, and<hr>
elements. - Split buttons are currently in the experimentation phase as we work out an accessible solution.
In the future, the content model is expected to expand to be more flexible, as the accessibility story for these experiences is fleshed out.
Conclusion
We're excited to see this feature progress through working groups and standards bodies, and share our progress as we actively build the prototype and evaluate the shape of this feature. If you encounter something that doesn't work as you expect, let us know!
And while this feature is still in development, we'd love to hear your feedback through this short feedback form.
Thank you for being a part of making sure we get this right and make it easier to build accessible, customizable form controls on the web!