The element has been part of HTML since the earliest days of the web, and for nearly all of that time, it has remained one of the most frustrating elements to work with. The appearance: base-select CSS property, now shipping in Chrome 133+, finally changes that equation. This article covers the new native approach that eliminates most of those costs: how appearance: base-select works, how to implement it step by step, and when a JavaScript dropdown library is still the right call.
How to Replace JavaScript Dropdown Libraries with Native Styled Selects
- Apply
appearance: base-selectto yourelement in CSS to unlock full styling control. - Add a
with achild inside theto create a custom trigger. - Style the dropdown popover using the
::picker(select)pseudo-element for backgrounds, borders, and shadows. - Customize the selected-option indicator via the
::checkmarkpseudo-element or hide it entirely. - Animate open/close transitions with
@starting-styleandtransition-behavior: allow-discrete. - Wrap all enhanced styles in
@supports (appearance: base-select)for progressive enhancement. - Test keyboard navigation, screen reader output, and fallback rendering in Firefox and Safari.
- Remove JavaScript dropdown libraries from components that no longer need them and verify bundle size savings.
Table of Contents
Why Has Been Broken for 30 Years
The element has been part of HTML since the earliest days of the web, and for nearly all of that time, it has remained one of the most frustrating elements to work with. Functionally, it does exactly what developers need: it collects user input from a list of options, participates natively in form submissions, supports keyboard navigation out of the box, and communicates semantics to assistive technologies. The problem has always been styling. The appearance: base-select CSS property, now shipping in Chrome 133+, finally changes that equation.
For decades, the browser’s operating system controlled how dropdowns render. Developers could adjust the trigger button’s font size or border to a limited degree, but the dropdown popup itself, the individual options, checkmarks, and selected state were all locked behind browser internals. This created an entire category of JavaScript libraries whose primary purpose is solving what is fundamentally a CSS problem.
This created an entire category of JavaScript libraries whose primary purpose is solving what is fundamentally a CSS problem.
React Select adds ~27 KB minified to a bundle. Choices.js adds ~20 KB. Headless UI contributes ~12 KB. Downshift is leaner at ~8 KB but still requires developers to reimplement rendering from scratch. Bundle sizes were measured via bundlephobia.com and may vary across versions — verify current sizes before making bundle-budget decisions. Every one of these libraries must also recreate keyboard navigation, ARIA roles, focus management, and screen reader announcements that the native provides for free. The result is a pattern of accessibility regressions (common examples documented in the WebAIM Million report and in numerous GitHub issue threads on React Select), increased bundle sizes, and maintenance burden.
This article covers the new native approach that eliminates most of those costs: how appearance: base-select works, how to implement it step by step, and when a JavaScript dropdown library is still the right call.
The Original Proposal
The Open UI community group, a W3C initiative focused on making built-in UI controls more customizable, initially proposed a brand-new HTML element called . The idea was to create a parallel to that came with styling hooks baked in from the start. Prototypes appeared in Chromium behind experimental flags, and early adopters began experimenting with the new element’s slot-based architecture.
The working group rearchitected the proposal. Rather than introducing a new element that would fragment the platform and force developers to choose between two similar controls, they decided to unlock the existing element itself. This meant developers could opt existing elements into the new styling behavior without changing markup, and would not need to learn a new element or migrate existing markup wholesale.
The Current Approach: Opt-in Styling via CSS
The mechanism that emerged is appearance: base-select, a CSS property value that opts a element into the new rendering behavior. When applied, the browser exposes the internal parts of the as stylable pseudo-elements and allows new child elements like and inside the .
Chrome 133, released in early 2025, is the first stable browser to ship support. Firefox and Safari have not shipped appearance: base-select in stable releases as of early 2025. Check caniuse.com and the respective browser standards position pages (Mozilla, WebKit) for current status before making adoption decisions. The opt-in nature of appearance: base-select is precisely why this approach won consensus: existing elements continue to render exactly as before unless a developer explicitly applies the property. Unsupporting browsers simply ignore appearance: base-select. However, the child of is non-conforming HTML (see Step 1 below) and may be ejected from the by non-Chrome parsers — test Firefox and Safari degradation behavior before deploying.
Understanding appearance: base-select and Its Inner Pseudo-Elements
How appearance: base-select Unlocks the Element
Applying appearance: base-select to a element fundamentally changes how the browser treats its rendering. The element shifts from an opaque, OS-delegated control to a fully stylable component. The browser retains accessible defaults: the element still participates in the accessibility tree as a listbox, keyboard navigation still works natively, and the element still submits form values without any behavioral change.
The minimal CSS required to opt in is exactly one declaration:
select {
appearance: base-select;
}
Once this is applied, the exposes several new styling targets. The trigger area becomes a stylable button. The dropdown popup becomes a CSS popover positioned via CSS anchor positioning. Individual options, checkmarks, and the reflected selected content all become addressable through pseudo-elements or new HTML child elements.
Key Pseudo-Elements and Parts Explained
::picker(select) — The Dropdown Popover
The ::picker(select) pseudo-element targets the dropdown popup that appears when the user opens the select. In the pre-base-select world, this popup was entirely controlled by the operating system and could not be styled at all. With the new behavior, it becomes a CSS popover that uses anchor positioning to attach itself to the trigger button. Developers can apply background colors, border-radius, box shadows, max-height with overflow scrolling, padding, and transitions.
Styling ::picker(select) requires that appearance: base-select is applied to the parent element first. Copy-pasting ::picker(select) styles without the appearance declaration will silently fail.
— Reflecting the Chosen Option
is a new HTML element that, when placed inside a within the , automatically mirrors the content of the currently selected into the trigger area. As of Chrome 133, the HTML Living Standard editors have not yet merged into the spec; it remains defined in the Open UI proposal. This is what enables rich content in the collapsed state. If each contains a flag emoji and a country name, the element will clone that same emoji and name into the button without any JavaScript syncing. The browser handles the content cloning automatically whenever the selection changes.
::checkmark — The Selected Indicator
The ::checkmark pseudo-element represents the indicator shown next to the currently active option in the dropdown. By default, browsers render a checkmark glyph. With appearance: base-select, developers can hide it entirely with display: none, restyle it with custom colors and sizing, or replace it visually using content or background images.
::checkmark is part of the Open UI proposal and Chrome 133+ supports it. Verify current syntax at MDN or the Open UI spec before use — the pseudo-element is not yet documented in the HTML Living Standard.
Here is an annotated example demonstrating all of these pseudo-elements and parts working together:
<select class="styled-select">
<button type="button">
<selectedcontent>selectedcontent>
button>
<option value="apple">🍎 Appleoption>
<option value="banana">🍌 Bananaoption>
<option value="cherry">🍒 Cherryoption>
select>
.styled-select {
appearance: base-select;
font-family: system-ui, sans-serif;
font-size: 1rem;
}
.styled-select button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
background: #fff;
cursor: pointer;
font: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.styled-select::picker(select) {
background: #fff;
border: 1px solid #ddd;
border-radius: 0.75rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
padding: 0.25rem;
max-height: 300px;
overflow-y: auto;
}
.styled-select option {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
}
.styled-select option:hover {
background: #f0f4ff;
}
.styled-select option::checkmark {
color: #4f46e5;
font-weight: bold;
}
Building a Fully Styled Custom Select — Step by Step
Step 1 — The HTML Structure
A element contains elements, just as it always has. The new additions are a element placed directly inside the (which becomes the trigger) and a element inside that button. Rich content, including emoji, images, or descriptive text, can go directly inside each .
This -inside- structure is non-conforming per the current HTML Living Standard. The Open UI proposal includes a spec change to allow as a child of , but the spec editors have not merged it. Browsers outside Chrome 133+ will apply error recovery and the may be moved out of the in the DOM, breaking the layout. Test in Firefox and Safari to verify degradation behavior before deploying to production.
<label for="country">Choose a countrylabel>
<select id="country" name="country" class="country-select">
<button type="button">
<selectedcontent>selectedcontent>
button>
<option value="">Select a country…option>
<option value="us">🇺🇸 United Statesoption>
<option value="gb">🇬🇧 United Kingdomoption>
<option value="de">🇩🇪 Germanyoption>
<option value="jp">🇯🇵 Japanoption>
<option value="br">🇧🇷 Braziloption>
<option value="au">🇦🇺 Australiaoption>
<option value="ca">🇨🇦 Canadaoption>
<option value="fr">🇫🇷 Franceoption>
select>
Step 2 — Base Styles with appearance: base-select
Opt in with appearance: base-select, then style the trigger button, the dropdown popover via ::picker(select), individual option states, and the checkmark.
.country-select {
appearance: base-select;
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
width: 100%;
max-width: 280px;
}
.country-select button {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.625rem 1rem;
border: 2px solid #d1d5db;
border-radius: 0.625rem;
background: #ffffff;
color: #111827;
font: inherit;
cursor: pointer;
transition: border-color 0.15s ease;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.country-select button:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
border-color: #4f46e5;
}
.country-select::picker(select) {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 0.375rem;
max-height: 260px;
overflow-y: auto;
}
.country-select option {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.95rem;
cursor: pointer;
}
.country-select option:hover {
background: #eef2ff;
}
.country-select option:checked {
background: #e0e7ff;
font-weight: 600;
}
.country-select option::checkmark {
color: #4f46e5;
margin-right: 0.5rem;
}
Step 3 — Animations and Transitions
::picker(select) supports open and close animations using @starting-style and transition-behavior: allow-discrete. The @starting-style at-rule defines the initial state before the popover enters, and transition-behavior: allow-discrete enables transitions on properties like display that are normally not animatable.
@starting-style requires Chrome 117+, Safari 17.5+, or Firefox 129+. If you need animations only where appearance: base-select is supported, wrap animation styles in a separate @supports block or treat the animation as an enhancement on top of the base feature.
.country-select::picker(select) {
opacity: 1;
transform: scaleY(1) translateY(0);
transition:
opacity 0.2s ease,
transform 0.2s ease,
display 0.2s ease;
transition-behavior: allow-discrete;
}
@starting-style {
.country-select::picker(select) {
opacity: 0;
transform: scaleY(0.9) translateY(-4px);
}
}
.country-select::picker(select):not(:open) {
opacity: 0;
transform: scaleY(0.9) translateY(-4px);
}
The transition-behavior: allow-discrete property is what enables the display property to animate between none and a visible value during the picker’s open/close cycle. It is specified as a separate property rather than inline in the transition shorthand. Verify this syntax matches Chrome 133’s current implementation in DevTools.
Step 4 — Responsive and Accessible Considerations
Tab moves focus to the trigger, Arrow Up and Arrow Down cycle through options, Enter selects, and Escape closes the dropdown. No JavaScript is required to enable any of this. ARIA roles are built in: the browser exposes the correct listbox and option roles automatically.
For mobile, ensuring adequate touch target sizing means the trigger button and individual options should maintain at least 44px of tappable height (per Apple’s Human Interface Guidelines; Android Material Design recommends 48dp). The animation block should also respect user preferences:
@media (prefers-reduced-motion: reduce) {
.country-select::picker(select) {
transition: none;
}
}
Comparison: Native vs. Popular JavaScript Libraries
Feature-by-Feature Comparison
| Feature | Native base-select | React Select | Downshift | Headless UI | Choices.js |
|---|---|---|---|---|---|
| Bundle size | 0 KB | ~27 KB min | ~8 KB min | ~12 KB min | ~20 KB min |
| Full keyboard nav | Built-in | Yes | Yes | Yes | Partial (no aria-activedescendant management) |
| Screen reader support | Native | Depends on ARIA implementation in consuming app | Yes | Yes | Partial (missing live-region announcements on selection change) |
| Custom option content | Yes | Yes | Yes | Yes | Yes |
| Form participation | Native | Hidden input | Manual | Manual | Via hidden input |
| Multi-select | Not supported (no active spec proposal) | Yes | Yes | Yes | Yes |
| Async search/filter | Not supported (requires JS by nature) | Yes | Yes | Manual | Yes |
| Browser support | Chrome 133+ | All modern | All modern | All modern | All modern |
When You Still Need a JavaScript Library
The native appearance: base-select approach does not cover every use case. Multi-select with a tag-based UI (users selecting and removing multiple items displayed as pills or chips), async typeahead with server-side search filtering, and virtualized lists for selects with more than ~500 static options all remain outside its scope. None of these have active spec proposals in Open UI. For teams that must support browsers lacking appearance: base-select, use progressive enhancement with @supports so those users receive a functional unstyled native , not a broken UI. A JavaScript library remains necessary only when you require visual parity across all browsers.
Removing a JavaScript dropdown library may regress the visual experience for users on non-Chrome browsers during the transition period. Use @supports to ensure those users still receive a functional native .
Integrating with React (and Other Frameworks)
Using appearance: base-select in a React Component
Because the styled select is still a native element, React integration is straightforward. Controlled and uncontrolled patterns work identically to any other in React. React version 18+ treats unrecognized elements like as custom elements. In React 17 and below, may cause a warning — test with your specific React version.
Create country-select.css with the CSS from Step 2 above. Place it in the same directory as your component file.
import { useState } from 'react';
import './country-select.css';
const countries = [
{ value: 'us', label: '🇺🇸 United States' },
{ value: 'gb', label: '🇬🇧 United Kingdom' },
{ value: 'de', label: '🇩🇪 Germany' },
{ value: 'jp', label: '🇯🇵 Japan' },
{ value: 'br', label: '🇧🇷 Brazil' },
];
function CountrySelect() {
const [country, setCountry] = useState('');
return (
<div>
<label htmlFor="country">Choose a countrylabel>
<select
id="country"
name="country"
className="country-select"
value={country}
onChange={(e) => setCountry(e.target.value)}
>
{}
{}
<button type="button" suppressHydrationWarning>
<selectedcontent suppressHydrationWarning />
button>
<option value="">Select a country…option>
{countries.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
option>
))}
select>
{country && (
<p aria-live="polite">
Selected: {country}
p>
)}
div>
);
}
export default CountrySelect;
Progressive Enhancement Strategy for Production
Feature detection is the correct approach for production deployments. The CSS.supports() API can check for appearance: base-select support at runtime, and the CSS @supports at-rule can scope styles so they only apply when the browser understands the property.
if (CSS.supports('appearance', 'base-select')) {
} else {
document
.querySelectorAll('select.country-select')
.forEach((el) => el.classList.add('js-select-fallback'));
}
select {
font: inherit;
padding: 0.5rem;
}
@supports (appearance: base-select) {
.country-select {
appearance: base-select;
}
.country-select button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
border: 2px solid #d1d5db;
border-radius: 0.625rem;
background: #fff;
font: inherit;
cursor: pointer;
}
.country-select::picker(select) {
border-radius: 0.75rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 0.375rem;
}
}
The fallback is simply the unstyled native , not a JavaScript library. Attempting to polyfill appearance: base-select would mean reimplementing exactly the kind of complex dropdown behavior that this feature exists to eliminate. Graceful degradation is the right approach.
The fallback is simply the unstyled native
, not a JavaScript library. Attempting to polyfillappearance: base-selectwould mean reimplementing exactly the kind of complex dropdown behavior that this feature exists to eliminate.
Implementation Checklist
- Add
appearance: base-selecttoCSS - Add
andinsidefor custom trigger content - Style
::picker(select)for the dropdown popover - Customize or hide
::checkmarkas needed - Add open/close animations with
@starting-styleandtransition-behavior: allow-discrete - Wrap enhanced styles in
@supports (appearance: base-select)for progressive enhancement - Test keyboard navigation (Tab, Arrow keys, Enter, Escape)
- Test with screen readers (VoiceOver, NVDA)
- Add
prefers-reduced-motionmedia query for animations - Verify form submission sends the correct
value - Audit existing JavaScript dropdown library usage for migration candidates
- Test
child parsing in Firefox and Safari to verify degradation behavior - Monitor caniuse.com for
appearance: base-selectFirefox and Safari shipping status
Common Pitfalls and Debugging Tips
The Picker Doesn’t Appear Styled
A common mistake is using appearance: none instead of appearance: base-select. While appearance: none removes some default browser styling from the trigger, it does not unlock the ::picker(select) pseudo-element or enable the new internal structure. The property value must be base-select specifically. Also verify the browser version: Chrome 133 or later is required (check chrome://version/). Earlier Chromium versions had the feature behind flags with different syntax from the era, and that syntax is no longer valid.
Not Reflecting the Selection
The element must be placed inside a which itself is a direct child of the . If the nesting is wrong, the browser will not clone the selected option’s content into the trigger. No JavaScript is needed for synchronization. The browser handles content cloning automatically whenever the selected option changes. If the element appears empty, check the DOM structure in DevTools to confirm the nesting order.
Popover Positioning Issues
The ::picker(select) popover uses CSS anchor positioning to attach to the trigger button. Ancestor elements with overflow: hidden, overflow: clip, or conflicting position values can clip or misposition the popover. Because the picker renders in the top layer (similar to or the Popover API), most stacking context issues are avoided, but overflow clipping on ancestors can still interfere with the anchor positioning calculations.
The Beginning of the End for Dropdown Libraries
The appearance: base-select property directly addresses the single most common reason JavaScript dropdown libraries exist: the inability to style native elements. For single-select UIs with fewer than ~100 static options, custom option content, branded styling, and smooth animations, the native element is now sufficient in supporting browsers. If your select needs server-side filtering, multi-select with tag removal, or virtualized rendering for large datasets, you still need JavaScript.
For single-select UIs with fewer than ~100 static options, custom option content, branded styling, and smooth animations, the native element is now sufficient in supporting browsers.
Adoption should be progressive. Wrapping enhanced styles in @supports (appearance: base-select) ensures that browsers without support continue to render a functional, accessible native . As Firefox and Safari ship their implementations, the surface area where JavaScript dropdown libraries remain necessary will narrow to those genuinely complex scenarios.
The Open UI community group’s specification work continues at https://open-ui.org/components/selectmenu/. MDN’s documentation for appearance: base-select tracks current browser compatibility. The @supports guard means you can ship appearance: base-select today with zero risk to unsupported browsers, and every dropdown instance you migrate removes a JavaScript dependency your users no longer need to download.

