docs.rodeo

MDN Web Docs mirror

Customizable select elements

{{PreviousMenuNext("Learn_web_development/Extensions/Forms/Advanced_form_styling", "Learn_web_development/Extensions/Forms/UI_pseudo-classes", "Learn_web_development/Extensions/Forms")}} 

This article explains how to use dedicated, modern HTML and CSS features together to create fully-customized {{htmlelement("select")}}  elements. This includes having full control over styling the select button, drop-down picker, arrow icon, current selection checkmark, and each individual {{htmlelement("option")}}  element.

Background

Traditionally it has been difficult to customize the look and feel of <select> elements because they contain internals that are styled at the operating system level, which can’t be targeted using CSS. This includes the drop-down picker, arrow icon, and so on.

Previously, the best available option — aside from using a custom JavaScript library — was to set an {{cssxref("appearance")}}  value of none on the <select> element to strip away some of the OS-level styling, and then use CSS to customize the bits that can be styled. This technique is explained in Advanced form styling.

Customizable <select> elements provide a solution to these issues. They allow you to build examples like the following, using only HTML and CSS, which are fully customized in supporting browsers. This includes <select> and drop-down picker layout, color scheme, icons, font, transitions, positioning, markers to indicate the selected icon, and more.

{{EmbedLiveSample("full-render", "100%", "410px")}} 

In addition, they provide a progressive enhancement on top of existing functionality, falling back to “classic” selects in non-supporting browsers.

You’ll find out how to build this example in the sections below.

What features comprise a customizable select?

You can build customizable <select> elements using the following HTML and CSS features:

In addition, the <select> element and its drop-down picker have the following behavior assigned to them automatically:

[!NOTE] You can check browser support for customizable <select> by viewing the browser compatibility tables on the reference pages for related features such as {{htmlelement("selectedcontent")}} , {{cssxref("::picker()", "::picker(select)")}} , and {{cssxref("::checkmark")}} .

Let’s look at all of the above features in action, by walking through the example shown at the top of the page.

Customizable select markup

Our example is a typical {{htmlelement("select")}}  menu that allows you to choose a pet. The markup is as follows:

<form>
  <p>
    <label for="pet-select">Select pet:</label>
    <select id="pet-select">
      <button>
        <selectedcontent></selectedcontent>
      </button>

      <option value="">Please select a pet</option>
      <option value="cat">
        <span class="icon" aria-hidden="true">🐱</span
        ><span class="option-label">Cat</span>
      </option>
      <option value="dog">
        <span class="icon" aria-hidden="true">🐶</span
        ><span class="option-label">Dog</span>
      </option>
      <option value="hamster">
        <span class="icon" aria-hidden="true">🐹</span
        ><span class="option-label">Hamster</span>
      </option>
      <option value="chicken">
        <span class="icon" aria-hidden="true">🐔</span
        ><span class="option-label">Chicken</span>
      </option>
      <option value="fish">
        <span class="icon" aria-hidden="true">🐟</span
        ><span class="option-label">Fish</span>
      </option>
      <option value="snake">
        <span class="icon" aria-hidden="true">🐍</span
        ><span class="option-label">Snake</span>
      </option>
    </select>
  </p>
</form>

[!NOTE] The aria-hidden="true" attribute is included on the icons so that they will be hidden from assistive technologies, avoiding the option values being announced twice (for example, “cat cat”).

The example markup is nearly the same as “classic” <select> markup, with the following differences:

This design allows non-supporting browsers to fall back to a classic <select> experience. The <button><selectedcontent></selectedcontent></button> structure will be ignored completely, and the non-text <option> contents will be stripped out to just leave the text node contents, but the result will still function.

Opting in to the custom select rendering

To opt-in to the custom select functionality and minimal browser base styles (and remove the OS-provided styling), your <select> element and its drop-down picker (represented by the ::picker(select) pseudo-element) both need to have an {{cssxref("appearance")}}  value of base-select set on them:

select,
::picker(select) {
  appearance: base-select;
}
* {
  box-sizing: border-box;
}

html {
  font-family: Arial, Helvetica, sans-serif;
}

body {
  width: 100%;
  padding: 0 10px;
  max-width: 480px;
  margin: 0 auto;
}

h2 {
  font-size: 1.2rem;
}

p {
  display: flex;
  gap: 10px;
}

label {
  width: fit-content;
  align-self: center;
}

select {
  flex: 1;
}

You can choose to opt-in just the <select> element to the new functionality, leaving the picker with the default OS styling, but in most cases, you’ll want to opt-in both. You can’t opt-in the picker without opting in the <select> element.

Once this is done, the result is a very plain rendering of a <select> element:

{{EmbedLiveSample("plain-render", "100%", "240px")}} 

You are now free to style this in any way you want. To begin with, the <select> element has custom {{cssxref("border")}} , {{cssxref("background")}}  (which changes on {{cssxref(":hover")}}  or {{cssxref(":focus")}} ), and {{cssxref("padding")}}  values set, plus a {{cssxref("transition")}}  so that the background change animates smoothly:

select {
  border: 2px solid #ddd;
  background: #eee;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #ddd;
}

Styling the picker icon

To style the icon inside the select button — the arrow that points down when the select is closed — you can target it with the {{cssxref("::picker-icon")}}  pseudo-element. The following code gives the icon a custom {{cssxref("color")}}  and a transition so that changes to its {{cssxref("rotate")}}  property are smoothly animated:

select::picker-icon {
  color: #999;
  transition: 0.4s rotate;
}

Next up, ::picker-icon is combined with the {{cssxref(":open")}}  pseudo-class — which targets the select button only when the drop-down picker is open — to give the icon a rotate value of 180deg when the <select> is opened.

select:open::picker-icon {
  rotate: 180deg;
}

Let’s have a look at the work so far — note how the picker arrow rotates smoothly through 180 degrees when the <select> opens and closes:

{{EmbedLiveSample("second-render", "100%", "250px")}} 

Styling the drop-down picker

The drop-down picker can be targeted using the {{cssxref("::picker()", "::picker(select)")}}  pseudo-element. As mentioned earlier, the picker contains everything inside the <select> element that isn’t the button and the <selectedcontent>. In our example, this means all the <option> elements and their contents.

First of all, the picker’s default black {{cssxref("border")}}  is removed:

::picker(select) {
  border: none;
}

Now the <option> elements are styled. They are laid out with flexbox, aligning them all to the start of the flex container and including a 20px {{cssxref("gap")}}  between each one. Each <option> is also given the same {{cssxref("border")}} , {{cssxref("background")}} , {{cssxref("padding")}} , and {{cssxref("transition")}}  as the <select>, to provide a consistent look and feel:

option {
  display: flex;
  justify-content: flex-start;
  gap: 20px;

  border: 2px solid #ddd;
  background: #eee;
  padding: 10px;
  transition: 0.4s;
}

[!NOTE] Customizable <select> element <option>s have display: flex set on them by default, but it is included in our stylesheet anyway to clarify what is going on.

Next, a combination of the {{cssxref(":first-of-type")}} , {{cssxref(":last-of-type")}} , and {{cssxref(":not()")}}  pseudo-classes is used to set an appropriate {{cssxref("border-radius")}}  on the top and bottom corners of the picker, and remove the {{cssxref("border-bottom")}}  from all <option> elements except the last one so the borders don’t look messy and doubled-up:

option:first-of-type {
  border-radius: 8px 8px 0 0;
}

option:last-of-type {
  border-radius: 0 0 8px 8px;
}

option:not(option:last-of-type) {
  border-bottom: none;
}

Next a different background color is set on the odd-numbered <option> elements using {{cssxref(":nth-of-type()", ":nth-of-type(odd)")}}  to implement zebra-striping, and a different background color is set on the <option> elements on focus and hover, to provide a useful visual highlight during selection:

option:nth-of-type(odd) {
  background: #fff;
}

option:hover,
option:focus {
  background: plum;
}

Finally for this section, a larger {{cssxref("font-size")}}  is set on the <option> icons (contained within <span> elements with a class of icon) to make them bigger, and the {{cssxref("text-box")}}  property is used to remove some of the annoying spacing at the block-start and block-end edges of the icon emojis, making them align better with the text labels:

option .icon {
  font-size: 1.6rem;
  text-box: trim-both cap alphabetic;
}

Our example now renders like this:

{{EmbedLiveSample("third-render", "100%", "370px")}} 

Adjusting the styling of the selected option contents inside the select button

If you select any pet option from the last few live examples, you’ll notice a problem — the pet icons cause the select button to increase in height, which also changes the position of the picker icon, and there is no spacing between the option icon and label.

This can be fixed by hiding the icon when it is contained inside <selectedcontent>, which represents the contents of the selected <option> as they appear inside the select button. In our example, it is hidden using {{cssxref("display", "display: none")}} :

selectedcontent .icon {
  display: none;
}

This does not affect the styling of the <option> contents as they appear inside the drop-down picker.

Styling the currently selected option

To style the currently selected <option> as it appears inside the drop-down picker, you can target it using the {{cssxref(":checked")}}  pseudo-class. This is used to set the selected <option> element’s {{cssxref("font-weight")}}  to bold:

option:checked {
  font-weight: bold;
}

Styling the current selection checkmark

You’ve probably noticed that when you open the picker to make a selection, the currently selected <option> has a checkmark at its inline-start end. This checkmark can be targeted using the {{cssxref("::checkmark")}}  pseudo-element. For example, you might want to hide this checkmark (for example, via display: none).

You could also choose to do something a bit more interesting with it — earlier on the <option> elements were laid out horizontally using flexbox, with the flex items being aligned to the start of the row. In the below rule, the checkmark is moved from the start of the row to the end by setting an {{cssxref("order")}}  value on it greater than 0, and aligning it to the end of the row using an auto {{cssxref("margin-left")}}  value (see Alignment and auto margins).

Finally, the value of the {{cssxref("content")}}  property is set to a different emoji, to set a different icon to display.

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "☑️";
}

[!NOTE] The ::checkmark and ::picker-icon pseudo-elements are not included in the accessibility tree, so any generated {{cssxref("content")}}  set on them will not be announced by assistive technologies. You should still make sure that any new icon you set visually makes sense for its intended purpose.

Let’s check in again on how the example is rendering. The updated state after the last three sections is as follows:

{{EmbedLiveSample("fourth-render", "100%", "410px")}} 

Animating the picker using popover states

The customizable <select> element’s select button and drop-down picker are automatically given an invoker/popover relationship, as described in Using the Popover API. There are many advantages that this brings to <select> elements; our example takes advantage of the ability to animate between popover hidden and showing states using transitions. The {{cssxref(":popover-open")}}  pseudo-class represents popovers in the showing state.

The technique is covered quickly in this section — read Animating popovers for a more detailed description.

First of all, the picker is selected using ::picker(select), and given an {{cssxref("opacity")}}  value of 0 and a transition value of all 0.4s allow-discrete. This causes all properties that change value when the popover state changes from hidden to showing to animate.

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

The list of transitioned properties features opacity, however it also includes two discrete properties whose values are set by the browser default styles:

[!NOTE] The allow-discrete value is needed to enable discrete property animations.

Next, the picker is selected in the showing state using ::picker(select):popover-open and given an opacity value to 1 — this is the end state of the transition:

::picker(select):popover-open {
  opacity: 1;
}

Finally, because the picker is being transitioned while it is moving from display: none to a display value that makes it visible, the transition’s starting state has to be specified inside a {{cssxref("@starting-style")}}  block:

@starting-style {
  ::picker(select):popover-open {
    opacity: 0;
  }
}

These rules work together to make the picker smoothly fade in and fade out when the <select> is opened and closed.

Positioning the picker using anchor positioning

A customizable <select> element’s select button and drop-down picker have an implicit anchor reference, and the picker is automatically associated with the select button via CSS anchor positioning. This means that an explicit association does not need to be made using the {{cssxref("anchor-name")}}  and {{cssxref("position-anchor")}}  properties.

In addition, the browser’s default styles provide a default position, which you can customize as explained in Positioning elements relative to their anchor.

In our demo, the position of the picker is set relative to its anchor by using the {{cssxref("anchor()")}}  function inside its {{cssxref("top")}}  and {{cssxref("left")}}  property values:

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

This results in the top edge of the picker always being positioned 1 pixel down from the bottom edge of the select button, and the left edge of the picker always being positioned 10% of the select button’s width across from its left edge.

Final result

After the last two sections, the final updated state of our <select> is rendered like this:

{{EmbedLiveSample("full-render", "100%", "410px")}} 

Customizing other classic select features

The above sections have covered all the new functionality available in customizable selects, and shown how it interacts with both classic single-line selects, and related modern features such as popovers and anchor positioning. There are some other <select> element features not mentioned above; this section talks about how they currently work alongside customizable selects:

Next up

In the next article of this module, we will explore the different UI pseudo-classes available to us in modern browsers for styling forms in different states.

See also

{{PreviousMenuNext("Learn_web_development/Extensions/Forms/Advanced_form_styling", "Learn_web_development/Extensions/Forms/UI_pseudo-classes", "Learn_web_development/Extensions/Forms")}} 

In this article

View on MDN