· 6 min read
Customizable select: style a native <select> with base-select
Customizable select lets you style a native <select> with CSS using appearance: base-select, rich options and selectedcontent. Here's how it works.

A customizable select is a normal HTML <select> that you can style fully with CSS after opting in with appearance: base-select. Options can hold icons, images and other markup, the dropdown can be styled like any element, and the control keeps its keyboard support, form submission and change events. It has shipped in Chrome and other Chromium browsers since version 135, arrived in Safari 27 this autumn, and is available behind a flag in Firefox.
For most of the web's history, the dropdown list was the one form control you could barely touch. Teams that needed a branded select reached for a JavaScript library, rebuilt the listbox from <div> elements, and then spent weeks putting back the keyboard handling and screen reader behavior that the native element had for free. This feature removes most of the reason to do that.
What is a customizable select?
It is the same <select> element with a new rendering mode. You turn the mode on with one CSS declaration applied to two things: the select itself and its picker.
select,
::picker(select) {
appearance: base-select;
}
The picker is the dropdown panel that appears when the select opens. ::picker(select) is the pseudo-element that targets it. You can opt in only the closed control and leave the picker with the operating system's look, but not the other way around.
Once opted in, several things change at once. The browser's HTML parser keeps elements inside <option> instead of throwing them away. The picker renders in the top layer, the same layer used by dialogs and popovers, so it is never clipped by overflow: hidden on an ancestor. And the picker is placed with CSS anchor positioning relative to the select, with built-in fallbacks that flip it if it would run off the screen. If you have read about HTML invoker commands, the model will feel familiar: the picker behaves like a popover that the browser opens and closes for you.
How does the markup work?
Two new pieces of markup matter. A <button> placed as the first child of the select replaces the default closed control. Inside it, the new <selectedcontent> element shows a copy of whatever the selected option contains.
<label for="lang">Language</label>
<select id="lang" name="lang">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="html">
<img src="/icons/html.svg" alt="">
<span>HTML</span>
</option>
<option value="css">
<img src="/icons/css.svg" alt="">
<span>CSS</span>
</option>
<option value="js">
<img src="/icons/js.svg" alt="">
<span>JavaScript</span>
</option>
</select>
<selectedcontent> is filled by cloning the chosen option's content, so the closed control shows the icon and label together. The button is inert: anything interactive you put inside it will not receive focus or clicks. Its only job is to display the current value.
Note the explicit value attributes. Without them, the select's value comes from the option's trimmed textContent, which gets less predictable once options contain nested markup. Setting value keeps your form data and JavaScript stable no matter what you render.
Which parts can you style?
The new mode exposes a small set of hooks:
| Selector | What it targets |
|---|---|
::picker(select) | The dropdown panel |
::picker-icon | The arrow inside the closed control |
::checkmark | The tick shown next to the selected option |
:open | The select while its picker is showing |
option:checked | The currently selected option |
selectedcontent | The copy of the selected option in the button |
A typical set of rules looks like this:
select::picker-icon {
transition: rotate 0.2s;
}
select:open::picker-icon {
rotate: 180deg;
}
option {
display: flex;
gap: 0.75rem;
align-items: center;
}
option::checkmark {
order: 1;
margin-left: auto;
}
/* keep the closed control compact */
selectedcontent img {
display: none;
}
::picker(select) {
top: calc(anchor(bottom) + 4px);
}
Options are laid out with flexbox, so order and margin-left: auto move the checkmark to the end of the row. Because the select is the picker's implicit anchor, you can use anchor() directly without declaring anchor-name.
Opening animations need one extra step. The picker moves between display: none and a visible state and in and out of the top layer, so the transition must include discrete properties and a starting style:
::picker(select) {
opacity: 0;
transition: opacity 0.2s, display 0.2s allow-discrete, overlay 0.2s allow-discrete;
}
select:open::picker(select) {
opacity: 1;
}
@starting-style {
select:open::picker(select) {
opacity: 0;
}
}
What do you give up?
Opting in is a trade, and a few native behaviors go away:
- The picker no longer draws outside the browser window. Classic selects can spill past the page edge; a customized one stays inside it.
- On phones, you do not get the operating system's built-in picker wheel or sheet. You get the in-page dropdown you styled.
- The closed control no longer sizes itself to the longest option. Give it a width if layout depends on it.
- Some frameworks strip the new elements or fail hydration during server-side rendering. Check your framework before rolling it out widely.
None of these are bugs. They follow from the picker becoming a regular, stylable part of the page.
How do you ship it safely?
The feature is a progressive enhancement. Browsers that don't understand it ignore the <button> and <selectedcontent>, strip non-text content from options, and render an ordinary select that still submits the right value. That only works if every option has real text.
WebKit's guidance on this is worth adopting as a rule: icons, swatches and images are additions to an option's text, never substitutes for it. A colour picker that shows only swatches is unreadable in a fallback browser and confusing to screen readers. Keep the label, even if you shrink it or hide it visually with a class that leaves it in the accessibility tree. Give decorative images alt="" so the name is not announced twice. And remember that ::checkmark and ::picker-icon are not exposed to assistive technology, so any meaning they carry needs to exist elsewhere.
Scope your styles with a feature query so older browsers get clean defaults instead of half-applied rules:
@supports (appearance: base-select) {
select,
::picker(select) {
appearance: base-select;
}
/* the rest of your custom select styles */
}
To try it, open a page with the markup above in a current Chrome, Edge or Safari, add the opt-in rule, and inspect the select in DevTools. The new pseudo-elements show up in the styles panel like any other.
Key takeaways
appearance: base-selecton the select and::picker(select)turns a native select into a fully stylable control.- A first-child
<button>with<selectedcontent>controls what the closed select shows. - Available in Chromium 135+ and Safari 27; Firefox has it behind a flag.
- You lose the OS mobile picker and auto-width, but keep keyboard, form and accessibility behavior.
- Always keep text in every option, and wrap the styles in
@supports.
FAQ
Can I use a customizable select in production today?
Yes, as a progressive enhancement. Users on supporting browsers get the styled version, and everyone else gets a working native select, as long as each option has text.
Does a customizable select still work with forms and JavaScript?
Yes. It submits with forms, takes part in validation, fires change events and exposes the same value and selectedIndex properties. Set explicit value attributes when options contain markup.
Does it support multiple selection?
Multi-select listboxes are covered by the same work but behave differently from the dropdown described here, so check current browser support separately before relying on them.