Custom select menus

appearance: base-select on <select>

Limited availability
  • Chrome135
  • Edge135
  • Firefox-
  • Safari27

Features it needs

Opting a select into appearance: base-select makes its picker a normal, styleable part of the page, so options can hold markup: an avatar, a swatch, a second line of muted text. The keyboard behaviour, the accessible role and the mobile picker stay the browser's, which is the part custom select components most often get wrong. This covers presentation. It does not turn a select into a combobox.

When this applies

Styling a select's options with markup, such as a flag or a two-line label.

The native approach

<select>
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="de">
    <span aria-hidden="true">DE</span> Germany
  </option>
  <option value="at">
    <span aria-hidden="true">AT</span> Austria
  </option>
</select>

<style>
  select,
  select::picker(select) {
    appearance: base-select;
  }

  select::picker(select) {
    border-radius: 0.5rem;
    padding: 0.25rem;
  }

  option:checked {
    font-weight: 600;
  }
</style>

MDN reference

When the dependency is still right

An answer that always says "the platform covers it" is worse than no answer. These are the cases where this one does not hold.

  • You need typeahead filtering, multi-select with tags, or options loaded asynchronously from a search. Those make it a combobox, and a select is the wrong element regardless of styling.
  • You need creatable options, where the user can add a value that is not in the list.
  • You need anything beyond Chromium today. Customizable select is limited availability. It degrades to a normal select, which is a safe fallback, but the design will not match.
  • You need grouped options with sticky group headers while scrolling.

Building it

This catalog stops at the swap. These guides go through the implementation and the fallbacks, and each one reads in the terminal through its own command.

Signs it was hand-rolled

No package is involved in any of these, so nothing would match in a package.json. If the code looks like one of them, this rule applies anyway, and the conditions above still decide.

  • a div with role=listbox and children with role=option, wired to arrow keys and Enter by hand

Packages this covers