Styling a <select> Dropdown with CSS appearance: base-select
Introduction
For as long as the web has had forms, <select> has been the control nobody could style. You could change the closed
box a little — its border, background and font — but the moment it opened, the list of options was drawn by the
operating system, in the operating system's font, with no room for an icon, a colour swatch or a second line of text.
So teams rebuilt it. A <div> with a click handler, a hidden list, a pile of ARIA attributes and keyboard handling
that almost never matched the real thing: type-ahead, Home and End, closing on Esc,
form submission, screen reader announcements and mobile behaviour all had to be recreated by hand.
appearance: base-select removes the reason to rebuild it. It opts a real <select> into a new, fully styleable
mode: the closed button, the dropdown, the arrow, the options and the checkmark all become ordinary boxes you can
style — and the options can hold real HTML. Everything the browser already got right stays exactly as it was.
The Opt-in
select,
::picker(select) {
appearance: base-select;
}Two selectors, because there are two parts to opt in. select is the closed box you click; ::picker(select) is the
dropdown that opens from it. Each can be opted in on its own, but you'll almost always want both.
With that one rule, the select drops its operating-system look for a plain, unopinionated base style — a bordered box, a list of options and a small arrow — that's ready to be styled like any other element.
Rich Content in Options
In a customisable select, an <option> can contain more than text. Images, icons and extra <span>s are kept and
rendered:
<select>
<option value="indigo">
<span class="swatch" style="--swatch: #818cf8"></span>
Indigo <small>#818cf8</small>
</option>
<option value="teal">
<span class="swatch" style="--swatch: #2dd4bf"></span>
Teal <small>#2dd4bf</small>
</option>
</select>This needed a change to the HTML parser as well as to CSS. Until now, the parser threw away any tags inside an
<option> and kept only the text. Browsers that support customisable select now keep those elements in the DOM.
Two rules keep it sensible. Content inside an option is for display: no interactive elements such as links, buttons or inputs. And the option's text still matters most, because it's what screen readers announce, what type-ahead matches against, and what older browsers show on their own.
The Anatomy
Every part of a customisable select can be targeted:
| Selector | What it styles |
|---|---|
select | The closed box you click — the select's button |
select > button | An optional custom button, replacing the default contents of the box |
selectedcontent | A live copy of the selected option, shown inside that button |
::picker-icon | The arrow in the closed box |
::picker(select) | The dropdown that holds the options |
option | Each option row |
option::checkmark | The tick shown beside the selected option |
select:open | The select while its picker is open |
option:checked | The currently selected option |
A typical set of styles touches most of them:
select::picker-icon {
transition: rotate 0.2s ease;
}
select:open::picker-icon {
rotate: 180deg;
}
::picker(select) {
padding: 0.25rem;
border: 1px solid #2a2f45;
box-shadow: 0 1rem 2rem -1rem rgb(0 0 0 / 0.6);
}
option {
display: flex;
align-items: center;
gap: 0.625rem;
}
option:checked {
background: rgb(129 140 248 / 0.3);
}
option::checkmark {
order: 1; /* move the tick to the end of the row */
margin-inline-start: auto;
}Options are ordinary boxes, so display: flex lays out the swatch, label and checkmark in a row, and order moves
the checkmark to the end without touching the HTML.
Showing the Selected Option with <selectedcontent>
By default, the closed select shows the selected option's text. To show the swatch or icon as well, give the select
a <button> as its first child, with a <selectedcontent> element inside:
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<option value="indigo">…</option>
<option value="teal">…</option>
</select>Whenever the selection changes, the browser copies the chosen option's content into <selectedcontent>. Because
it's a copy, it can be styled differently from the option it came from — showing the swatch and name but hiding the
hex code, for example:
selectedcontent {
display: flex;
align-items: center;
gap: 0.625rem;
}
selectedcontent small {
display: none;
}Positioning and Animating the Picker
The picker is drawn in the top layer
— the same layer as popovers and modal dialogs — so it's never clipped by
an overflow: hidden parent and never needs a z-index. It's placed with anchor positioning, tied to the select:
by default it opens below, at least as wide as the select, and flips above when there isn't room underneath.
Since it's anchor-positioned, position-area can move it. To open the picker to the right of the select, for
example:
::picker(select) {
position-area: span-bottom right;
}Opening and closing can be animated with the same technique used for popovers and dialogs. Transition display and
overlay with allow-discrete, so the picker stays visible until its fade-out ends, and use @starting-style to
give the fade-in a starting point (see
why display needs allow-discrete):
::picker(select) {
transition:
opacity 0.2s ease,
translate 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
select:not(:open)::picker(select) {
opacity: 0;
translate: 0 -0.5rem;
}
@starting-style {
select:open::picker(select) {
opacity: 0;
translate: 0 -0.5rem;
}
}For visitors who've asked their system for reduced motion, keep the fade but drop the slide and the spinning arrow. The picker still clearly opens and closes, just without movement:
@media (prefers-reduced-motion: reduce) {
select::picker-icon {
transition: none;
}
select:not(:open)::picker(select) {
translate: none;
}
@starting-style {
select:open::picker(select) {
translate: none;
}
}
}Try It Live
The sandbox has one <select> whose options hold a colour swatch, a name and a hex code. Open it with the mouse,
then try it from the keyboard: Tab to it, press Space, use the arrow keys, type a letter to
jump to an option, and press Esc to close. Untick appearance: base-select to see the same markup
as a classic select, and untick Animate to compare the picker with and without its transition. The accent
preview beside the select takes on whichever colour you choose, and the readout shows the selected value and the
CSS behind the current state.
Customisable select Playground
A real <select> whose options hold colour swatches, styled with appearance: base-select, ::picker(select), ::picker-icon, ::checkmark and <selectedcontent>, with toggles to compare it against the classic select and to animate the picker open and closed.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Customisable select Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>Styling a Real select with appearance: base-select</h1> <p>One native <code><select></code> with colour swatches in its options. Open it with the mouse or the keyboard, then switch <code>base-select</code> off to compare it with the select browsers have drawn until now. The badge beside it shows the chosen colour as an accent.</p> </header> <p class="support-note" id="support-note" hidden>This browser doesn't support <code>appearance: base-select</code> yet, so you'll see a regular select with plain-text options — the fallback every visitor without support gets.</p> <fieldset class="control-group"> <legend>Options</legend> <label class="checkbox-field"> <input type="checkbox" id="custom-toggle" checked> <code>appearance: base-select</code> on the select and its picker </label> <label class="checkbox-field"> <input type="checkbox" id="animate-toggle" checked> Animate the picker open and closed </label> </fieldset> <section class="demo-area" aria-label="Customisable select"> <div class="stage"> <label class="select-label" for="colour-select">Accent colour</label> <select class="colour-select is-custom is-animated" id="colour-select"> <button type="button"><selectedcontent></selectedcontent></button> <option value="indigo" selected><span class="swatch" style="--swatch: #818cf8"></span><span class="option-text">Indigo <small>#818cf8</small></span></option> <option value="teal"><span class="swatch" style="--swatch: #2dd4bf"></span><span class="option-text">Teal <small>#2dd4bf</small></span></option> <option value="amber"><span class="swatch" style="--swatch: #fbbf24"></span><span class="option-text">Amber <small>#fbbf24</small></span></option> <option value="rose"><span class="swatch" style="--swatch: #fb7185"></span><span class="option-text">Rose <small>#fb7185</small></span></option> <option value="lime"><span class="swatch" style="--swatch: #a3e635"></span><span class="option-text">Lime <small>#a3e635</small></span></option> </select> <span class="accent-preview" id="accent-preview">Accent preview</span> </div> </section> <output class="readout" id="readout" aria-live="polite"></output> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
What Stays the Same
base-select changes how the select looks, not how it behaves. It's still a real <select>, so:
- Its value is submitted with the form, and
select.valueand thechangeevent work as before - Keyboard support is built in: arrow keys, type-ahead, Home, End, Enter and Esc
- Screen readers announce it as a combobox with a list of options, using each option's text
<label for>,required,disabledand:invalidall behave as they always have- Light dismiss works: clicking outside the picker closes it
That's the real gain over a hand-built dropdown. You only write CSS; the behaviour you'd otherwise have to rebuild is already there and already correct.
Browser Support and Fallbacks
Customisable select shipped in Chromium browsers (Chrome and Edge 135) in 2025. Firefox and Safari are working on it but haven't shipped it at the time of writing.
The fallback is built in. A browser that doesn't recognise base-select ignores the declaration and draws an
ordinary select. Its parser drops the swatch <span>s and the extra <button>, and each option shows its text. The
control still works perfectly; it just looks like a regular select.
That's why the option's text should be able to stand on its own. "Indigo" is a complete label without its swatch; an option containing nothing but an icon would be blank in every browser without support.
Common Pitfalls
1. Opting In Only the Select
appearance: base-select on select alone customises the closed box, but the picker keeps its native look. Put it
on ::picker(select) as well for a fully custom dropdown.
2. Options with No Text
An option that only contains an image or icon has no accessible name, nothing for type-ahead to match and nothing to show in a fallback. Always include text, and hide it visually only if the design truly needs it.
3. Putting Interactive Content in Options
Links, buttons and inputs inside an <option> aren't allowed — the option itself is the interactive element. Keep
option content to images, icons and text.
4. Reaching for a Custom Dropdown Anyway
If a design calls for a searchable list, multiple tags or free-text entry, that's a different control — a combobox —
and <select> isn't the right base for it. But for "pick one from a list", which is most dropdowns, a customisable
<select> now covers the designs that used to justify a rebuild.
Key Takeaway
appearance: base-select on select and ::picker(select) turns the least styleable form control into one of the
most: options with real HTML, a <selectedcontent> copy in the closed box, and pseudo-elements for the picker,
arrow and checkmark. It keeps the native select's keyboard support, accessibility and form behaviour, and falls back
to a working plain select everywhere else. For brand-coloured checkboxes, radios and sliders to sit beside it, see
Theming Native Controls with accent-color.
Changelog
- — Added cross-references to posts 'Mastering CSS z-index: Stacking Contexts, isolation, and Layering Bugs' and 'An Interactive Guide to CSS Transitions'
- — Initial publication.
Share this tutorial
Spotted a mistake or something outdated in this tutorial? Suggest a correction.