12k
All articles

Styling a Select Dropdown With base-select

Style native select dropdowns with CSS using appearance: base-select. Explore picker, options, icons, accessibility, browser support, fallbacks, and key trade-offs.

OpenReplay Team
OpenReplay Team
Styling a Select Dropdown With base-select

Setting appearance: base-select on a <select> and on its ::picker(select) pseudo-element lets you style the whole control with CSS: the button, the dropdown, the arrow, the checkmark and every option.

Many teams maintain a custom dropdown component for one reason only: the native <select> couldn’t be styled to match the design. Keyboard handling, focus management and ARIA wiring came along as a side effect. appearance: base-select removes that reason.

This guide walks through the full styling surface, from opting in to rich options, and covers what you give up. For classic appearance: none techniques, styling select elements with modern CSS covers the broader picture but only touches on base-select briefly. Older approaches are covered in styling select dropdowns without JavaScript.

Key Takeaways

  • appearance: base-select must be set on both select and ::picker(select). Setting it on the picker alone does nothing.
  • The picker is a popover in the top layer, anchored to the select button through an implicit anchor reference, so you don’t need z-index, portals or anchor-name.
  • <selectedcontent> goes inside a <button> that is the first child of the <select>, and it shows a clone of the selected option in the closed control.
  • Every option should contain text, because icon-only options give screen readers nothing to announce and show up empty in unsupported browsers.
  • base-select gives up the mobile OS picker, rendering outside the browser pane, and automatic sizing to the longest option.

The Component You Can Delete

A custom select component usually exists only to fix the native <select> element’s appearance, and in doing so it takes on a lot of behaviour. The Chrome team’s launch post says that from Chrome 135 you can style the <select> element with CSS, and it keeps the accessibility and standard behaviour of a built-in control. WebKit followed in Safari 27.0, where the real <select> can now be restyled from top to bottom. Once a browser supports it, the main reason to keep a div-based listbox is gone, and so is the behaviour you had to rebuild to support it.

How Do You Opt In to appearance: base-select?

To opt in, apply appearance: base-select to both the <select> and its picker. MDN’s customizable select guide explains that the select can be opted in by itself, but the picker only works once the select is opted in too. WebKit’s guidance on customizable select recommends wrapping these enhancements in @supports (appearance: base-select). Keep every later rule inside the same block, so browsers without support get the plain select.

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
  /* all following rules go here */
}
PartSelectorNotes
Closed controlselectThe select button
Arrow::picker-iconNot in the accessibility tree
Open state:openMatches while the picker is shown
Dropdown::picker(select)Everything except the first-child button
Each choiceoptiondisplay: flex by default
Selected choiceoption:checked
Selection mark::checkmarkNot in the accessibility tree
Closed-state contentselectedcontentClone of the selected option

Style the Closed Control

The closed control is the select element. The arrow is ::picker-icon, and :open matches while the picker is showing, so you can rotate the arrow when the select opens.

select {
  border: 2px solid #d4d4d8;
  background: #f4f4f5;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #e4e4e7;
}

select::picker-icon {
  color: #71717a;
  transition: 0.4s rotate;
}

select:open::picker-icon {
  rotate: 180deg;
}

The transition on rotate makes the arrow turn smoothly each time the picker opens and closes.

Style the Open List

The open dropdown list of a customizable select is ::picker(select). The browser groups everything inside the <select> into it, apart from the button that comes first. Options are flex containers by default. :checked targets the selected option, and ::checkmark targets the mark next to it.

::picker(select) {
  border: none;
  border-radius: 8px;
}

option {
  display: flex;
  gap: 16px;
  padding: 10px;
}

option:hover,
option:focus {
  background: #ede9fe;
}

option:checked {
  font-weight: bold;
}

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "✔";
}

Setting order: 1 with margin-left: auto moves the checkmark from the start of the row to the end.

Positioning and Animation

In a customizable select, the dropdown is a popover that opens in the top layer. The browser ties it to the select button with CSS anchor positioning on its own, so you don’t need a z-index, a portal or any anchor-name wiring. The button and picker have the same invoker/popover relationship that the invoker commands guide describes for buttons and popovers. The default styles also move the picker to another side if it would run off the screen, and you can set your own position with anchor():

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

To fade the picker in and out, transition it between its popover states:

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

:open::picker(select) {
  opacity: 1;
}

@starting-style {
  :open::picker(select) {
    opacity: 0;
  }
}

allow-discrete lets the discrete display and overlay properties take part in the transition. Without it, the picker would leave the top layer before the fade-out could be seen. @starting-style sets the opacity the fade-in starts from.

Rich Options and selectedcontent

Place <selectedcontent> inside a <button> that is the first child of the <select>. It shows a clone of the selected option’s content in the closed control, and you can style it separately from the options in the dropdown. Options can now contain markup such as icons and images.

<label for="status">Status</label>
<select id="status" name="status">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="draft">
    <span class="icon" aria-hidden="true">📝</span>
    <span class="option-label">Draft</span>
  </option>
  <option value="review">
    <img class="icon" src="review.svg" alt="">
    <span class="option-label">In review</span>
  </option>
  <option value="published">
    <span class="icon" aria-hidden="true">✅</span>
    <span class="option-label">Published</span>
  </option>
</select>

MDN notes that icons can make the closed button taller and push the arrow out of place. You can hide them in the closed state only:

selectedcontent .icon {
  display: none;
}

The options in the open picker keep their icons. The select button is inert, so links or buttons placed inside it can’t be focused or clicked on their own, and the whole thing acts as one button. MDN also points out that extra content in the button can change what screen readers report as the select’s value, so keep the button’s content to <selectedcontent>.

What Does the Browser Handle for You?

With base-select, the browser handles the things a custom dropdown has to reimplement: keyboard interaction, focus, top-layer stacking, form submission of the value, and screen reader semantics. WebKit says keyboard support and accessibility come built in, because it is still the browser’s own control. Replaying user sessions on a div-based dropdown tends to expose keyboard and focus bugs, and those are the bugs a native select avoids by design. For a sense of how much code the manual approach takes, see this guide to building an accessible dropdown select by hand.

Two rules still apply:

  • Keep text in every option. WebKit’s main rule is that each option needs real text, or an accessible text attribute. Text you hide visually is still read by screen readers. Decorative images take alt="" and let the text label carry the meaning.
  • Treat custom icons as decoration. Screen readers ignore anything you add with content on ::checkmark and ::picker-icon, so a custom arrow or checkmark is never read aloud.

Existing JavaScript that reads the select’s value keeps working. If an option has no value attribute, the select’s value is the selected option’s textContent with whitespace trimmed, so always set value when options contain rich markup.

Support, Fallback and Trade-offs

appearance: base-select ships in Chrome 135 and later, in other Chromium-based browsers such as Edge, and in Safari 27.0. Firefox has started adding it behind a preference: the appearance: base-select value landed in Firefox 149 behind a flag, but it isn’t turned on by default. MDN still labels it limited availability, so check the compatibility tables on MDN’s reference pages. Safari ships its own default base-select styles, so unstyled controls won’t look the same in every browser.

Browsers without base-select support ignore the <button> and <selectedcontent> markup. They render an ordinary select that works normally, with each option reduced to its text. That fallback only makes sense if the text is there.

Chrome’s post also lists three behaviours you lose:

  • The open list stays inside the browser window.
  • On phones and tablets, you get the browser’s picker instead of the operating system’s own.
  • The select’s width no longer follows its longest option, so set a width on select, ::picker(select) yourself.

Base-select also changes how the HTML parser reads what’s inside a <select>. MDN warns that this can clash with JavaScript frameworks: some block the features, and with others, server-side rendering can break hydration. Test the feature in your framework before you remove your existing component.

Conclusion

A native select with appearance: base-select gives you control over styling while the browser keeps handling keyboard, focus, stacking and accessibility. To try it, pick one simple custom dropdown, rebuild it inside an @supports block with text in every option, and check it in a browser that doesn’t support base-select. If it still works as a plain select there, and the trade-offs above are acceptable, you can remove the custom component.

FAQs

What is the difference between appearance: none and appearance: base-select on a select?

appearance: none only strips the native styling of the closed button. The dropdown list and its options still use the browser or operating system picker, and most CSS doesn't reach them. appearance: base-select, set on both select and ::picker(select), also lets you style the picker, options, arrow and checkmark, and it allows rich HTML inside options. Browsers without support ignore it and keep the classic control.

How do I detect base-select support in JavaScript?

Call CSS.supports('appearance', 'base-select'). It returns true only in browsers that recognise the value. It checks the same thing as the CSS condition @supports (appearance: base-select), so you can use it to decide whether to load a fallback dropdown component or add a class that turns on custom styles. Use feature detection instead of user agent sniffing, because each engine added support at a different time.

Does base-select work with select multiple or the size attribute?

Yes, in listbox mode. The multiple attribute, or a size value greater than 1, shows the options as a box on the page instead of a button that opens a popup. From Chrome 145, base-select also works in this mode, for both single and multiple selection. It doesn't yet cover the multi-select popup you get with multiple and size=1. A customizable listbox has no picker, so ::picker(select), ::picker-icon and selectedcontent don't apply. Check MDN's compatibility tables for support in other engines.

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.