DEV Community

Cover image for Customizable <select>: now in Chrome and Safari
richi.codes
richi.codes

Posted on Originally published at richi.codes

Customizable <select>: now in Chrome and Safari

This note has an interactive demo. Try it in the original

The <select> you can finally style is no longer a Chrome-only experiment. Safari 27, released on September 14, 2026, added "the customizable <select> element, allowing custom styling with appearance: base-select and custom content with the <selectedcontent> HTML element". Chrome has shipped it since version 135. So two of the three big engines now give you a real <select>, with real keyboard support and real form behavior, that you can design like any other component.

Open the demo above and pick a theme. The colored circles, the two-line options, the checkmark, the rotating arrow and the picker that fades in are all one native <select>. Then tick "Remove base-select": the same HTML falls back to the classic control, and the whole page still works.

Why this matters

For twenty years the answer to "can I style this dropdown?" was "rebuild it in JavaScript". That meant a div pretending to be a listbox, your own keyboard handling, your own ARIA, and a form field that no longer submitted by itself. The customizable select keeps the native element and only opens up its insides. As Chrome's announcement puts it, it's "accessible, standardized and CSS styleable".

How it works

You opt in with one CSS property, on the select and on its picker:

select,
::picker(select) {
  appearance: base-select;
}
Enter fullscreen mode Exit fullscreen mode

MDN notes you can opt in the select alone, but you "can't opt-in the picker without opting in the <select> element".

Then the HTML can carry more than text. A <button> as the first child becomes the closed control, and <selectedcontent> inside it shows a copy of the chosen option:

<select id="theme">
  <button><selectedcontent></selectedcontent></button>
  <option value="ember">
    <span class="swatch" aria-hidden="true"></span>
    <span class="txt"><span>Ember</span> <small>richi.codes orange</small></span>
  </option>
  <!-- more options -->
</select>
Enter fullscreen mode Exit fullscreen mode

Each option in the demo is exactly that: a decorative circle plus a name and a short description.

The parts you style are new pseudo-elements and pseudo-classes:

/* the arrow, and how it turns while open */
select::picker-icon { transition: rotate .3s ease; }
select:open::picker-icon { rotate: 180deg; }

/* the selected option and its checkmark */
option:checked { background: color-mix(in oklab, var(--sa) 18%, transparent); }
option::checkmark { order: 1; margin-left: auto; content: "✓"; }

/* the clone in the closed button: hide the description */
selectedcontent small { display: none; }
Enter fullscreen mode Exit fullscreen mode

The picker is a popover in the top layer, so it animates like one. This is the entry animation from the demo, with @starting-style for the first frame:

::picker(select) {
  opacity: 0;
  transform: translateY(-8px) scale(.96);
  transition: opacity .25s, transform .35s,
              overlay .35s allow-discrete, display .35s allow-discrete;
}
select:open::picker(select) { opacity: 1; transform: none; }
@starting-style {
  select:open::picker(select) { opacity: 0; transform: translateY(-8px) scale(.96); }
}
Enter fullscreen mode Exit fullscreen mode

It's also implicitly anchored to the button, so you can nudge it with anchor() if you need to.

Browser support and gotchas

  • Chrome 135+ and Safari 27, not Firefox yet. MDN's compatibility data lists appearance: base-select in Firefox 149 only behind two preferences, and <selectedcontent> and ::picker(select) aren't there. MDN marks it as limited availability, not Baseline.
  • The fallback is the classic select. Without support, the browser ignores the button and keeps the options as plain text. Wrap your styles in @supports (appearance: base-select) so the classic control isn't half-styled. The demo does that and also rewrites the option text as "Ember · richi.codes orange" in the fallback, so the name and description don't run together.
  • WebKit's golden rule. The WebKit team's advice is to "always provide text content or accessible text attributes for your option elements". Icons go with aria-hidden="true" or alt="", and the meaning lives in text. MDN adds that ::checkmark and ::picker-icon aren't in the accessibility tree, so whatever you put in their content isn't announced.
  • The value can change if you skip value. Without a value attribute, the select's value is the option's trimmed textContent. Add a description inside the option and your submitted value changes too. I checked it in Chromium and WebKit with a bare <option>: the value followed the text. Chrome's post says it plainly: options that use the value attribute are unaffected.
  • <selectedcontent> goes stale. It copies the option when the selection changes, and only then. In Chromium and WebKit I changed an option's text after loading: the button kept the old text until I selected again. The demo hits this itself. It translates the options into English after the page has parsed, so it re-selects the current option to refresh the copy. Frameworks that re-render options need to do the same.
  • The clone doesn't bring the option's styles. In the demo, each option's colors live in custom properties on the <option>. The copy inside <selectedcontent> lives elsewhere in the tree and doesn't inherit them, so its circle takes the active theme's colors from the root instead.
  • Native behaviors you give up. Chrome lists them: the select doesn't render outside the browser pane, it doesn't trigger the mobile operating system's own components, and it stops taking the width of the longest option.

What to keep an eye on

  • Firefox: the work is behind preferences. When it ships, this becomes Baseline and you can drop most plan B code.
  • Framework support. MDN warns that some frameworks block these features and others hit hydration errors with server-side rendering.
  • More "base" form controls. Chrome's post says there are more "base" elements yet to be completed. This is just the first one.

Sources


Originally published at richi.codes.

Top comments (0)