Future of CSS: Select styling without the hacks

For years the element has been notoriously difficult to style. Developers had to either accept the browser’s default look or resort to JavaScript-heavy solutions. But why has it been this way for so long? Why Can’t Be Styled? The component is a form control, meaning browsers handle much of its behaviour natively. This includes dropdown logic (have you noticed the options list can overflow the window?), keyboard navigation, and accessibility features. However, because these controls are deeply integrated into the OS, styling has been largely restricted. Workarounds: From jQuery UI to shadcn/ui Since native styling wasn’t an option, developers turned to libraries: jQuery UI (early 2010s): Wrapped in a div, replaced it with a . Custom Dropdowns (2015–2022): React/Vue solutions often replaced entirely. shadcn/ui (modern approach): Uses Radix UI under the hood to create accessible dropdowns. While these solutions worked, they came with trade-offs: extra JavaScript, potential accessibility issues, and performance overhead. Enter base-select: A New Approach with a Caveat With the introduction of the base-select property, browsers will allow full CSS styling of without overriding most native functionality. This means: No need for JavaScript to handle dropdowns. Full control over appearance while keeping built-in accessibility. Potentially faster rendering and better performance. Important Note: The base-select property is currently experimental and only available in Chrome 134+. Browser support is limited, so use this with caution in production environments. Styling with base-select When styling the select element you must add appearance: base-select; to both the select and select::picker(select). select { appearance: base-select; &::picker(select) { appearance: base-select; } } This tells the browser to allow the element to be styled by CSS rather than using the system's default appearance. Understanding ::picker(select): This pseudo-element represents the dropdown listbox (the "picker") of the element. It allows you to style the listbox independently. There are a few different pseudo-classes and pseudo-elements that are exposed to modify the select element. I've documented a few useful ones down below. select { appearance: base-select; /* style the 'button' */ &::picker(select) { appearance: base-select; /* style the 'listbox' */ } &::picker-icon { /* style the 'button' icon */ } &:not(:open) { /* style the 'button' when closed */ &::picker(select) { /* style the 'listbox' when closed */ } } &:open { /* style the 'button' when open */ &::picker(select) { /* style the 'listbox' when open */ } } & option { /* style the options */ &::checkmark { /* style the checkmark on the checked option */ } &:checked { /* style the checked option */ } } } Demo: CSS-Only shadcn/ui Select Let’s recreate shadcn/ui’s select component using only CSS. Here’s how we can do it: Styling the itself to match shadcn/ui’s look. Customising the dropdown’s appearance. Ensuring accessibility is maintained. Remember: This demo only works in Chrome 134+. If you can’t test it, I’ve included a GIF below so you can still see it in action. Markup The HTML structure remains simple. The element allows us to display and style the selected option separately, and we add a chevron icon to mimic shadcn/ui. Fruits Select a Fruit Apple Banana Blueberry Grapes Pineapple Styling the Select Button To start, we need to apply appearance: base-select and set some basic styles. select { appearance: base-select; color: #71717a; background-color: transparent; width: 180px; box-sizing: border-box; padding: 0.5rem 0.75rem; border: 1px solid #e4e4e7; border-radius: calc(0.5rem - 2px); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; } Positioning the content and icon: select > button { display: flex; width: 100%; font-family: inherit; color: currentColor; } select > button > svg { margin: 0 0 0 auto; width: 1.2rem; height: 1.2rem; } Styling the Dropdown Listbox The listbox must be styled separately, ensuring it appears smoothly when opened. We're using the relatively new starting-style to allow us to animate from display: none. select::picker(select) { appearance: base-select; border: 1px solid #e4e4e7; padding: 0.25rem; margin-top: 0.25rem; border-radius: calc(0.5rem - 2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); cursor: default; transition: opacity 225ms ease-in-out, transform 225ms ease-in-out; transform-origin: top; transform: translateY(0); opacity: 1; @starting-style { transform: translateY(-0.25rem) scale(0.95); opacity: 0; } } Enhancing Accessibi

Mar 10, 2025 - 22:26
 0
Future of CSS: Select styling without the hacks

For years the Be Styled?

The in a div, replaced it with a

    .
  • Custom Dropdowns (2015–2022): React/Vue solutions often replaced without overriding most native functionality. This means:
    • No need for JavaScript to handle dropdowns.
    • Full control over appearance while keeping built-in accessibility.
    • Potentially faster rendering and better performance.

    Important Note: The base-select property is currently experimental and only available in Chrome 134+. Browser support is limited, so use this with caution in production environments.

    Styling element to be styled by CSS rather than using the system's default appearance.

    Understanding ::picker(select): This pseudo-element represents the dropdown listbox (the "picker") of the itself to match shadcn/ui’s look.

  • Customising the dropdown’s appearance.
  • Ensuring accessibility is maintained.

Remember: This demo only works in Chrome 134+. If you can’t test it, I’ve included a GIF below so you can still see it in action.

Fallback animation

Markup

The HTML structure remains simple. The element allows us to display and style the selected option separately, and we add a chevron icon to mimic shadcn/ui.


Styling the Select Button

To start, we need to apply appearance: base-select and set some basic styles.

select {
 appearance: base-select;
 color: #71717a;
 background-color: transparent;
 width: 180px;
 box-sizing: border-box;
 padding: 0.5rem 0.75rem;
 border: 1px solid #e4e4e7;
 border-radius: calc(0.5rem - 2px);
 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
 cursor: pointer;
}

Positioning the content and icon:

select > button {
 display: flex;
 width: 100%;
 font-family: inherit;
 color: currentColor;
}

select > button > svg {
 margin: 0 0 0 auto;
 width: 1.2rem;
 height: 1.2rem;
}

Styling the Dropdown Listbox

The listbox must be styled separately, ensuring it appears smoothly when opened. We're using the relatively new starting-style to allow us to animate from display: none.

select::picker(select) {
 appearance: base-select;
 border: 1px solid #e4e4e7;
 padding: 0.25rem;
 margin-top: 0.25rem;
 border-radius: calc(0.5rem - 2px);
 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  0 2px 4px -2px rgba(0, 0, 0, 0.1);
 cursor: default;
 transition: opacity 225ms ease-in-out, transform 225ms ease-in-out;
 transform-origin: top;
 transform: translateY(0);
 opacity: 1;

 @starting-style {
  transform: translateY(-0.25rem) scale(0.95);
  opacity: 0;
 }
}

Enhancing Accessibility & Interactions

Improve focus visibility and ensure placeholder text stands out:

select:focus-visible {
 outline: 2px solid #a1a1aa;
 outline-offset: -1px;
}

select:has(option:not([hidden]):checked) {
 color: #18181b;
}

Custom Checkmark

We can replace the default checkmark with a custom SVG.

select option::after {
 content: "";
width: 1rem;
 height: 1.5rem;
 margin-left: auto;
 opacity: 0;
 background: center / contain no-repeat
  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2318181b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'%3E%3C/path%3E%3C/svg%3E");
}

select option:checked::after {
 opacity: 1;
}

Conclusion

The