← Back
[W] Writing

You can finally style the real select

For most of the web's life, <select> was the one form control you couldn't design. You could style the closed box a little. The list that opens was off limits.

That's changed. The colour picker below is a real <select>, with no JavaScript in its behaviour. Try it below, then turn on the parts.

Every outline is ordinary CSS aimed at one part of a single select. In a browser without support, you get the native control, with the same value.

Why you couldn't style it

<select> has been in HTML since HTML 2.0 in 1995. From the start, browsers didn't draw its list as part of the page. They handed it to the operating system, or drew it in a separate window of their own, the same way a native app shows a menu.

That's why it could do things nothing else on the page could. It could hang off the bottom of the browser window. On a phone, it became the system's own picker.

It's also why CSS couldn't reach it. A stylesheet styles boxes in the page's layout. The open list wasn't in the page at all, so there was nothing for a selector to match.

So design systems rebuilt it. A <div> that looks like a select, a list with role="listbox", and then all the behaviour the real one had: focus that moves through options, keys that open and close it, typing a letter to jump, closing when you click outside, a hidden input so the form still submits, and positioning so the list doesn't get cut off by a parent with overflow: hidden.

Every one of those is a place for a bug. Most custom selects ship with a few.

One property opens it up

The new switch is a value of appearance. You set it on the select and on its picker, and the browser stops handing the list to the operating system. It draws it inside the page, as ordinary elements that CSS can style.

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

It's two declarations on purpose. The first gives you the closed button. The second gives you the list. You can opt into one without the other.

Once you're in, the select has real parts you can target. Some are pseudo-elements, and some are HTML you write yourself.

A customisable select, from the outside in

<select>the element and the form value

<button>optional, first child, the closed trigger

<selectedcontent>a copy of the chosen option

::picker-iconthe arrow

::picker(select)the popover holding everything else

<option>now allowed to hold images and spans

::checkmarkshown on the selected option

Only the button and selectedcontent are elements you write. The other three are parts the browser creates, which you can style but not remove from the tree.

The HTML for the demo looks like this:

<select name="accent">
  <button><selectedcontent></selectedcontent></button>
  <option value="ocean">
    <span class="swatch" aria-hidden="true"></span>
    Ocean
  </option>
</select>

<selectedcontent> is the clever bit. When you choose an option, the browser clones its contents into it, swatch included. You don't write any code to keep the button in sync.

The button is inert, so anything inside it can't take focus or clicks of its own. The select stays one control.

The list is a popover

Here's the part that ties it together. The open list isn't a special case. It's a popover, the same thing you'd make with the popover attribute.

That means it opens in the top layer, the layer the browser keeps above everything else on the page. A parent with overflow: hidden or a low z-index can't clip it. It also closes when you click outside it, the way any popover does.

And it's placed with anchor positioning, with the select as its anchor. The HTML spec writes the default placement as real CSS in the browser's own stylesheet:

::picker(select) {
  min-inline-size: anchor-size(self-inline);
  position-area: self-block-end span-self-inline-end;
  position-try-order: most-block-size;
  position-try-fallbacks:
    self-block-start span-self-inline-end,
    self-block-end span-self-inline-start,
    self-block-start span-self-inline-start;
}

Read it as a sentence. Start below the select, at least as wide as it, aligned to its start edge. If there isn't room, try above, then flipped sideways. And try first whichever position leaves the most height.

That's straight from the spec's rendering section, and it's the logic every JavaScript dropdown library ships its own version of. Because it's plain CSS, you can override any line. Want the list centred under the button? Change position-area.

Since it's a popover, it also animates like one. Transition opacity and display with allow-discrete, and give the opening state an @starting-style. The demo's 140ms fade is about eight lines.

What you get for free

The real cost of a rebuilt select is the behaviour you have to rewrite. Press keys on this one and watch the list tick itself.

The page only listens for key presses and then checks what the browser did. It doesn't move focus or change the value itself.

None of that was written by this page. The browser also keeps the parts you can't see in a demo: the value submits with the form and resets with it, required works, clicking the label focuses it, and assistive technology still gets a real select, not a pile of divs pretending.

The value still comes from the option's value attribute, or its text if there's no attribute. So select.value and your form handler don't change. Chrome's announcement does warn that code reading the selected option's text may need a look, now that options can contain more than text.

There is one trade, and it's worth knowing before you ship. Opting in means the list is drawn in the page, so you give up the operating system's picker. Chrome's launch post is plain about it: a base-select doesn't render outside the browser window, doesn't use the mobile operating system's picker, and no longer sizes itself to its longest option.

On a phone, that means your styled list instead of the system sheet. For a short list of six colours, that's fine. For a list of 200 countries, the system picker is probably still the better experience, and you can keep it by putting appearance: base-select inside @media (pointer: fine).

Shipping it today

As of November 2026, it's in Chrome and Edge 135 and later, and in Safari 27 on macOS and iOS, according to MDN's compatibility data. Firefox has an implementation behind preferences since version 149, but it isn't on by default.

The fallback is the best part. In a browser without support, the parser drops the <button>, <selectedcontent> and anything that isn't text inside an option. What's left is an ordinary select with the same options and the same value. So wrap the styling in a feature query and you're done:

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
}

That only works if every option still has words in it. WebKit calls this the golden rule of customizable select: a swatch or an icon is an addition to an option's text, never a replacement for it. Remove the name to make room for the swatch, and older browsers show a list of blank rows.

This took a long time to get right. The Open UI group first proposed a brand-new element called <selectmenu>, then renamed it <selectlist>. In the end, the intent to ship landed on upgrading <select> itself, so every existing form gets the new behaviour by opting in, and loses nothing by not.

I think that's the right shape for it. The browser already knew how a select should behave. The only thing missing was a way for CSS to say how it should look.

Next