Future of CSS: Select styling without the hacks
Andrew Bone

Andrew Bone @link2twenty

About: A British web developer, that is passionate about web accessibility.

Location:
Britain, Europe
Joined:
Jun 8, 2017

Future of CSS: Select styling without the hacks

Publish Date: Mar 10
32 14

For years the <select> 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 <select> Be Styled?

The <select> 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

  • jQuery UI (early 2010s): Wrapped <select> in a div, replaced it with a <ul>.
  • Custom Dropdowns (2015–2022): React/Vue solutions often replaced <select> 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 <select> 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 <select> 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;
 }
}
Enter fullscreen mode Exit fullscreen mode

This tells the browser to allow the <select> 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 <select> 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 */
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

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 <select> 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 <selectedcontent> element allows us to display and style the selected option separately, and we add a chevron icon to mimic shadcn/ui.

<select>
  <button>
    <selectedcontent></selectedcontent>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="m6 9 6 6 6-6"></path>
    </svg>
  </button>
  <optgroup>
    <label>Fruits</label>
    <option value="" hidden disabled selected>Select a Fruit</option>
    <option>Apple</option>
    <option>Banana</option>
    <option>Blueberry</option>
    <option>Grapes</option>
    <option>Pineapple</option>
  </optgroup>
</select>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
 }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The <select> component is finally getting the flexibility it deserves. With base-select, we can create beautiful dropdowns without JavaScript. If you're interested in reading more check out the open ui explainer.

What do you think of base-select? Are you excited to ditch JavaScript-heavy dropdowns?

Thanks for reading! If you'd like to connect, here are my Twitter, BlueSky, and LinkedIn profiles. Come say hi 😊

Comments 14 total

  • Ben Sinclair
    Ben SinclairMar 11, 2025

    I have some concerns about this - a lot of devices have their own native way of presenting choices (like the rolodex thing on iOS for example). In the same way as with the JS solutions, we don't want to be replacing the familiar, optimised native behaviour - at least not in all circumstances. And if we do it only in some cases, then the app becomes inconsistent.

    I'm not sure what the solution is, though.

    • Andrew Bone
      Andrew BoneMar 11, 2025

      Fortunately you only see the replaced listmenu on desktop leaving mobile with its native feel, that's how it works on android at least we'll have to wait and see if its the same on iOS.

  • nadeem zia
    nadeem ziaMar 11, 2025

    Good information provided, thanks

  • Lai Kok Wui
    Lai Kok WuiMar 11, 2025

    i didnt know we can add codepen in the post cool

  • Learn Computer Academy
    Learn Computer AcademyMar 12, 2025

    Nice overview of base-select! It’s great to see CSS addressing the styling issue without needing JavaScript. The native styling options for the picker and options look promising, and the shadcn/ui demo is a solid example. Limited browser support is a challenge for now, but I’m looking forward to using this once it’s more widely available. Good post!

  • codagonist
    codagonistMar 17, 2025

    To bad these sort of 'fixes' aren't cross-browser or -device and if I'm not mistaken semantically incorrect. Interesting post nonetheless..

    • Andrew Bone
      Andrew BoneMar 17, 2025

      This will be cross browser. It's new to the spec it just so happens that chrome were the first to implement it. 😊

  • Oscar
    OscarMar 17, 2025

    How are you highlighting your code here?

    Image description

    • Andrew Bone
      Andrew BoneMar 17, 2025

      code block with no colors example

      ... to specify the language:

      code block with colors example

      More details in the editor guide.

      • Oscar
        OscarMar 17, 2025

        I meant the "glow" on the &::picker(select). Sorry for the confusion!

        • Andrew Bone
          Andrew BoneMar 17, 2025

          Oh, that's because the linter Forem are using doesn't know what it is. The glow is the error state.

          • Oscar
            OscarMar 17, 2025

            Oh, interesting. Thanks!

  • Gabriel Ibañez
    Gabriel IbañezMar 18, 2025

    Thanks for the resource

Add comment