Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Technical review: Document customizable <select> elements #38470

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

chrisdavidmills
Copy link
Contributor

@chrisdavidmills chrisdavidmills commented Mar 5, 2025

Description

Chrome 134 supports customizable <select> elements (see https://chromestatus.com/feature/5737365999976448).

This includes a multitude of sub-features, which are as follows:

  • The <selectedcontent> element mirrors the currently-selected <option> contents. This is the content visible inside the closed <select> element, otherwise known as the select <button>.
  • The ::picker(select) pseudo-element, which targets the entire contents of the select drop-down menu, otherwise known as the picker.
  • The appearance property value base-select, which opts the <select> element and the ::picker(select) pseudo-element into the browser-defined default styles and behavior for customizable select.
  • The :open pseudo-class, which can now target the select <button> when the picker (::picker(select)) is open.
  • The ::picker-icon pseudo-element, which targets the picker icon inside the select <button> — the little down-facing arrow on the inline-end side.
  • The :checked pseudo-class, which can now target the currently-selected <option> element.
  • The ::checkmark pseudo-element, which targets the checkmark placed inside the currently-selected <option> element to provide a visual indication of which one is selected.

In addition, the select <button> and the picker have the following behavior assigned to them automatically:

  • They have an invoker/popover relationship, as specified by the Popover API.
  • They have an implicit anchor reference, meaning that the picker is automatically positioned relative to the select <button> via CSS anchor positioning.

This PR aims to document all of the above new/updated features, and:

  • provide a guide to creating customizable <select>s.

Motivation

Additional details

Related issues and pull requests

Fixes #37857

BCD: mdn/browser-compat-data#26171

@chrisdavidmills chrisdavidmills requested a review from a team as a code owner March 5, 2025 18:09
@chrisdavidmills chrisdavidmills requested review from hamishwillee and removed request for a team March 5, 2025 18:09
@chrisdavidmills chrisdavidmills marked this pull request as draft March 5, 2025 18:09
@github-actions github-actions bot added Content:Learn Learning area docs size/m [PR only] 51-500 LoC changed labels Mar 5, 2025
Copy link
Contributor

github-actions bot commented Mar 5, 2025

Preview URLs (30 pages)
Flaws (40)

Note! 16 documents with no flaws that don't need to be listed. 🎉

URL: /en-US/docs/Learn_web_development/Extensions/Forms/UI_pseudo-classes
Title: UI pseudo-classes
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/void_element is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/void_element which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Other_form_controls
Title: Other form controls
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/void_element is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/void_element which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Sending_and_retrieving_form_data
Title: Sending form data
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/submit_button is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/submit_button which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Basic_native_form_controls
Title: Basic native form controls
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/void_element is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/void_element which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML5_input_types
Title: The HTML5 input types
Flaw count: 2

  • broken_links:
    • /en-US/docs/Web/API/validityState/typeMismatch is ill cased
  • macros:
    • Macro produces link /en-US/docs/Web/API/validityState/typeMismatch which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Your_first_form
Title: Your first form
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/void_element is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/void_element which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Sending_forms_through_JavaScript
Title: Sending forms through JavaScript
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/progressive_web_apps is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/progressive_web_apps which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls
Title: How to build custom form controls
Flaw count: 6

  • broken_links:
    • /en-US/docs/Web/API/element/classList is ill cased
    • /en-US/docs/Web/API/element/querySelector is ill cased
    • /en-US/docs/Web/API/element/querySelectorAll is ill cased
  • macros:
    • Macro produces link /en-US/docs/Web/API/element/classList which is a redirect
    • Macro produces link /en-US/docs/Web/API/element/querySelector which is a redirect
    • Macro produces link /en-US/docs/Web/API/element/querySelectorAll which is a redirect

URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML_forms_in_legacy_browsers
Title: HTML forms in legacy browsers
Flaw count: 1

  • unknown:
    • No generic content config found

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select
Title: Customizable select elements
Flaw count: 3

  • broken_links:
    • /en-US/docs/Glossary/top_layer is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/top_layer which is a redirect
    • Can't resolve /en-US/docs/Web/CSS/@starting-state

URL: /en-US/docs/Web/API/Popover_API/Using
Title: Using the Popover API
Flaw count: 8

  • broken_links:
    • /en-US/docs/Glossary/top_layer is ill cased
    • /en-US/docs/Glossary/top_layer is ill cased
    • /en-US/docs/Glossary/inset_properties is ill cased
    • /en-US/docs/Glossary/top_layer is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/top_layer which is a redirect
    • Macro produces link /en-US/docs/Glossary/top_layer which is a redirect
    • Macro produces link /en-US/docs/Glossary/inset_properties which is a redirect
    • Macro produces link /en-US/docs/Glossary/top_layer which is a redirect

URL: /en-US/docs/Web/HTML/Element/button
Title: <button>: The Button element
Flaw count: 4

  • broken_links:
    • /en-US/docs/Glossary/user_agent is ill cased
    • /en-US/docs/Glossary/accessible_name is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/user_agent which is a redirect
    • Macro produces link /en-US/docs/Glossary/accessible_name which is a redirect

URL: /en-US/docs/Web/HTML/Element/select
Title: <select>: The HTML Select element
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/user_agent is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/user_agent which is a redirect

URL: /en-US/docs/Web/CSS/CSS_anchor_positioning/Using
Title: Using CSS anchor positioning
Flaw count: 2

  • broken_links:
    • /en-US/docs/Glossary/inset_properties is ill cased
  • macros:
    • Macro produces link /en-US/docs/Glossary/inset_properties which is a redirect
External URLs (85)

URL: /en-US/docs/Learn_web_development/Extensions/Forms/UI_pseudo-classes
Title: UI pseudo-classes


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Other_form_controls
Title: Other form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Sending_and_retrieving_form_data
Title: Sending form data


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Basic_native_form_controls
Title: Basic native form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML5_input_types
Title: The HTML5 input types


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Your_first_form
Title: Your first form


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Form_validation
Title: Client-side form validation


URL: /en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls
Title: How to build custom form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/How_to_structure_a_web_form
Title: How to structure a web form


URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML_forms_in_legacy_browsers
Title: HTML forms in legacy browsers


URL: /en-US/docs/Web/API/HTMLSelectedContentElement
Title: HTMLSelectedContentElement


URL: /en-US/docs/Web/HTML/Element/selectedcontent
Title: <selectedcontent>: The selected option display element

(comment last updated: 2025-03-10 17:16:57)

@github-actions github-actions bot added Content:HTML Hypertext Markup Language docs size/l [PR only] 501-1000 LoC changed and removed size/m [PR only] 51-500 LoC changed labels Mar 7, 2025
@github-actions github-actions bot added the Content:WebAPI Web API docs label Mar 7, 2025
@github-actions github-actions bot added the Content:CSS Cascading Style Sheets docs label Mar 10, 2025
@chrisdavidmills chrisdavidmills marked this pull request as ready for review March 10, 2025 13:18
@chrisdavidmills chrisdavidmills requested review from a team as code owners March 10, 2025 13:18
@chrisdavidmills chrisdavidmills requested review from dipikabh and removed request for a team March 10, 2025 13:18
@bsmth bsmth removed the request for review from a team March 10, 2025 15:37
@github-actions github-actions bot added size/xl [PR only] >1000 LoC changed and removed size/l [PR only] 501-1000 LoC changed labels Mar 10, 2025
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 work alongside customizable selects:

- [`<select multiple>`](/en-US/docs/Web/HTML/Attributes/multiple)
- : When the `multiple` attribute is set on customizable `<select>` elements, the styling works pretty much how you would expect — the styling applied to the `<option>` elements is presented as-is in the multiple select box, and the outer box itself is giving the same styling as the select `<button>` in the single select. Any styling related to the showing and hiding of the picker is ignored.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customizable select is not supported yet for select multiple. I am planning on working on this as a follow up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting to know — I did some testing with multiple on my customizable select demo, and it turned out looking OK. It kind of "just worked". I've updated the wording to the following, to make it clear that this isn't official supported yet:

There isn't currently any official support specified for the multiple attribute on customizable <select> elements, however the styling works generally OK. The <option> elements are displayed as-is inside the multiple select box, and the outer box itself is given the same styling as the select <button> in the single select.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the way I implemented it, I expect that applying appearance: base-select to a <select multiple> will not change anything at all. The option elements inside them should also not be rendering anything interesting besides text content yet. Did you see otherwise? Can you share an example, or is there one inside here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing — have a look at https://customizable-select-test.glitch.me/. This is the same code as the demo I explain in the article, but with multiple set on the <select> element.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the demo! I can see that removing appearance:base-select doesn't do anything. I can also see that it renders the same way in firefox, which hasn't implemented customizable select yet. ::checkmark also doesn't show up.

<select multiple> on desktop does not use a popup document or native APIs to render the options, so normal CSS stuff like this is likely to already work. The icons also show up in the options since they are just unicode, but actual <img> elements or doing layout of stuff inside of an <option> won't work.

These are all things I'm going to address when implementing customizable select support for <select multiple>, as well as improved keyboard behavior and getting consistent in-page vs popup rendering which is a big issue with the current select multiple. Go ahead and try opening that demo on a mobile device: whatwg/html#8189

@github-actions github-actions bot added the merge conflicts 🚧 [PR only] label Mar 14, 2025
Copy link
Contributor

This pull request has merge conflicts that must be resolved before it can be merged.

@github-actions github-actions bot removed the merge conflicts 🚧 [PR only] label Mar 14, 2025
Copy link
Contributor

github-actions bot commented Mar 14, 2025

Preview URLs (31 pages)
Flaws (4)

Note! 28 documents with no flaws that don't need to be listed. 🎉

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select
Title: Customizable select elements
Flaw count: 2

  • macros:
    • Can't resolve /en-US/docs/Glossary/inert
    • Can't resolve /en-US/docs/Web/CSS/@starting-state

URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML_forms_in_legacy_browsers
Title: HTML forms in legacy browsers
Flaw count: 1

  • unknown:
    • No generic content config found

URL: /en-US/docs/Web/HTML/Element/selectedcontent
Title: <selectedcontent>: The selected option display element
Flaw count: 1

  • macros:
    • Can't resolve /en-US/docs/Glossary/inert
External URLs (85)

URL: /en-US/docs/Learn_web_development/Extensions/Forms/Basic_native_form_controls
Title: Basic native form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Form_validation
Title: Client-side form validation


URL: /en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls
Title: How to build custom form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/How_to_structure_a_web_form
Title: How to structure a web form


URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML_forms_in_legacy_browsers
Title: HTML forms in legacy browsers


URL: /en-US/docs/Learn_web_development/Extensions/Forms/HTML5_input_types
Title: The HTML5 input types


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Other_form_controls
Title: Other form controls


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Sending_and_retrieving_form_data
Title: Sending form data


URL: /en-US/docs/Learn_web_development/Extensions/Forms/UI_pseudo-classes
Title: UI pseudo-classes


URL: /en-US/docs/Learn_web_development/Extensions/Forms/Your_first_form
Title: Your first form


URL: /en-US/docs/Web/API/HTMLSelectedContentElement
Title: HTMLSelectedContentElement


URL: /en-US/docs/Web/HTML/Element/selectedcontent
Title: <selectedcontent>: The selected option display element

(comment last updated: 2025-03-19 13:03:18)

@chrisdavidmills
Copy link
Contributor Author

@josepharhar thanks for the review; I've answered your comments. Have you still got any more content to look at, or can I consider your tech review finished?


{{LearnSidebar}}

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, checkbox to indicate which item is currently selected, each individual {{htmlelement("option")}} element, and more besides.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"arrow icon, checkbox" should be "arrow icon, and checkbox"?

Also maybe say "checkmark" instead of "checkbox"?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"and more besides" at the end also reads kind of weird to me. Is it meant to say that there are additional things to customize which are not listed in this paragraph?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback. I've updated the sentence to:

This includes having full control over styling the select button, drop-down picker, arrow icon, current selection checkmark, and each individual {{htmlelement("option")}} element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in my next commit)


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, etc.

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 cna be styled. This technique is explained in [Advanced form styling](/en-US/docs/Learn_web_development/Extensions/Forms/Advanced_form_styling).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"bits that cna" -> "bits that can"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed; thanks!

- The {{cssxref(":checked")}} pseudo-class, which targets the currently-selected `<option>` element.
- The {{cssxref("::checkmark")}} pseudo-element, which targets the checkmark placed inside the currently-selected `<option>` element to provide a visual indication of which one is selected.

In addition, the select `<button>` and the picker have the following behavior assigned to them automatically:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like here and above we are starting to talk about "the select <button>" without properly introducing it - it's a very new concept in customizable select.

Maybe in the list above we should add an item to say that you're allowed to put a <button> inside a select, and that it replaces the in-page "button" rendering of the select element in order to allow the author to render whatever they want there?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also be worth mentioning somewhere that this <button> should be the first child element of the select element in order to work properly, and the browser makes it inert by default which prevents it from having interactive children that work as you might expect.

This is done via interactivity: inert, so authors can remove the inertness if they want but it might also break some event handling stuff we've built.

interactivity is also shipping after customizable select so right now its hard coded to be inert, we are going to properly move it to a UA style in a bit - undoing inertness won't work yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I've added in a separate bullet about the <button> first child, and updated the surrounding bullets so they make sense with it in context.

And thanks for letting me know about the inert nature of the select <button>. I literally just pushed a PR earlier today that documents the interactive property and adds a glossary entry for "inert". That is much smaller than this PR, so I reckon it should be published first.

I've added a note about this to the customizable selects page, with a link to the "inert" glossary item, in preparation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done via interactivity: inert, so authors can remove the inertness if they want

this sounds like a bad idea. why would that be allowed if it was made inert on purpose?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's probably a bad idea. We probably shouldn't suggest that authors can make it not inert.

- The {{cssxref(":open")}} pseudo-class, which targets the select `<button>` when the picker (`::picker(select)`) is open.
- The {{cssxref("::picker-icon")}} pseudo-element, which targets the picker icon inside the select `<button>` — the down-facing arrow on the inline-end side.
- The {{cssxref(":checked")}} pseudo-class, which targets the currently-selected `<option>` element.
- The {{cssxref("::checkmark")}} pseudo-element, which targets the checkmark placed inside the currently-selected `<option>` element to provide a visual indication of which one is selected.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customizable select also officially sanctions putting <div> and non-interactive phrasing content inside <option>s, whereas before you were only supposed to put text content in it. Not sure if that's worth calling out in this list especially since you have examples below where <span> is used, but it could be called out here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see it called out below:

Traditionally, <option> elements could only contain text, but in a customizable select you can include markup structures including images and more besides. In our example, each <option> contains two {{htmlelement("span")}} elements containing an icon and a text label respectively, allowing each one to be styled and positioned independently.

I guess that's fine as-is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is mostly fine, but just to make sure, I've added a sentence to the first bullet of the first list:

These work just the same as in "classic" selects, except that they have additional permitted content types.


This is nearly the same as "classic" `<select>` markup, with the following differences:

- The `<button><selectedcontent></selectedcontent></button>` structure represents the select {{htmlelement("button")}}; the {{htmlelement("selectedcontent")}} element contains a clone of the currently-selected {{htmlelement("option")}} (created using {{domxref("Node.cloneNode", "cloneNode()")}} under the hood). This allows you to select and [adjust the styling of the selected `<option>` contents as shown inside the select button](#adjusting_the_styling_of_the_selected_option_contents_inside_the_select_button). If this structure is not included in your markup, the browser will place the selected option content inside the select button implicitly, but you won't be able to select it using CSS.
Copy link

@josepharhar josepharhar Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this structure is not included in your markup, the browser will place the selected option content inside the select button implicitly

It's the <button> specificially which makes the browser stop putting the selected option's text inside its default in-page button. You could leave out the <selectedcontent> and still have full styling capabilities, but you'll have to decide how you want to render the button in other ways.

I thought about reading more content about <selectedcontent> before bringing this up, but we made them separate elements so that authors can do more interesting stuff like put content adjacent to the <selectedcontent> element or use a framework primitive to do cloning instead, or not do any cloning at all. The framework thing is something we got feedback about last year but I haven't really tested out, so probably not worth calling out until we try this out in react to find out some possible alternative best practices for react or something.

Not using <selectedcontent> may cause some accessibiilty issues in some cases though, so if you make a full on demo with that I might want @scottaohara take a look at it. (maybe he should review all this too since a lot of people will probably read this in order to learn customizable select!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a think about this, and updated it to:

If this structure is not included in your markup, the browser will fall back to rendering the selected option's text inside the default button, and you won't be able to style it as easily.

Probably not worth saying anything more just for now?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's fine, thanks! Sorry for the wall of text here and elsewhere, there's so much nuance that I can't stop thinking about 😵‍💫

}
```

You can choose to opt-in one or the other, but in most cases you'll want to opt-in both.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The picker can't be opted in to base-select unless the select element itself is too. I agree that in most cases you'll want to opt-in both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the sentence to:

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

}
```

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<options> -> <option>s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, both instances, ta!

- {{cssxref("display")}}
- : The `display` values changes from `none` to `block` when the popover changes state from hidden to shown. This needs to be animated to ensure that other transitions are visible.
- {{cssxref("overlay")}}
- : The `overlay` value changes from `none` to `auto` when the popover changes state from hidden to shown, to promote it to the {{glossary("top layer")}}, then back again when it is hidden to remove it. This needs to be animated to ensure the removal of the popover from the top layer is deferred until the transition completes, again ensuring the transition is visible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling this out!

again ensuring the transition is visible

If you don't transition overlay then it should still be visible, but it might cause UA styles to unexpectedly change during the exit animation, and most importantly, the popover won't be in the top layer during the exit animation. These things might not make a difference in simple cases. This text is probably fine as-is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the wording is OK. When we originally documented this stuff, we thought quite a lot about the wording, and decided the top layer removal part was the most useful for explaining why this is needed.

```css live-sample___full-render
::picker(select) {
opacity: 0;
transition: all 0.4s allow-discrete;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never tried using transition:all in popover animations, but sounds like it works which is great! Certainly easier to read than explicitly doing display, overlay, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it works pretty nicely!

Copy link
Contributor

@scottaohara scottaohara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't finished looking through all of this yet.

@@ -367,9 +369,11 @@ Let's talk about some specifics of each of these types of control, highlighting

### Selects and datalists

In modern browsers, selects and datalists are generally not too bad to style provided you don't want to vary the look and feel too much from the defaults.
Selects and datalists are generally not too bad to style.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is it that datalists can be styled now? i thought this was just limited to styling select's popup.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, customizable select does not affect datalists. Datalist styling is still very limited.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling that out; that was a bad edit. In my next commit I've adjusted the text to say that datalist styling is still limited and make the situation clearer:

Selects are generally not too bad to style. Many modern browsers now support Customizable select elements, a set of HTML and CSS features that together enable full customization of <select> elements and their contents just like any regular DOM elements. In supporting browsers and codebases, you don't need to worry about these legacy techniques any more.

Datalist styling is still limited.

For datalists, and for selects in browsers that don't support customizable selects, you can get an OK level of customization provided you don't want to vary the look and feel too much from the defaults.

etc.

We've managed to get the basic look of the boxes looking pretty uniform and consistent. The datalist control is `<input type="text">` anyway, so we knew this wouldn't be a problem.
Many modern browsers now support [Customizable select elements](/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select), a set of HTML and CSS features that together enable full customization of `<select>` elements and their contents just like any regular DOM elements. In supporting browsers and codebases, you don't need to worry about these legacy techniques any more.

Even in non-supporting browsers and codebases, you can get a reasonable level of customization provided you don't want to vary the look and feel too much from the defaults. We've managed to get the basic look of the boxes looking pretty uniform and consistent. The datalist control is `<input type="text">` anyway, so we knew this wouldn't be a problem.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Even in non-supporting browsers and codebases, you can get a reasonable level of customization provided you don't want to vary the look and feel too much from the defaults. We've managed to get the basic look of the boxes looking pretty uniform and consistent. The datalist control is `<input type="text">` anyway, so we knew this wouldn't be a problem.
Even in non-supporting browsers and codebases, you can get a reasonable level of customization provided you don't want to vary the look and feel too much from the defaults. We've managed to get the basic look of the boxes looking pretty uniform and consistent. The `datalist` invoking control is an `<input type="text">` anyway, so we knew this wouldn't be a problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Scott; updated in my next commit.

- Plain old {{htmlelement("select")}}, {{htmlelement("option")}}, and {{htmlelement("optgroup")}} elements. These work just the same as in "classic" selects, except that they have additional permitted content types. The new features are built on top of existing select functionality as progressive enhancements, meaning that customizable selects can be designed to fall back to "classic" selects in non-supporting browsers.
- A {{htmlelement("button")}} element included as the first child inside the `<select>` element, which wasn't previously allowed in "classic" selects. When this is included, it replaces the default "button" rendering of the closed `<select>` element. You can then include arbitary content inside the `<button>`, to render whatever you want inside the closed `<select>`. This is comonly known as the **select {{htmlelement("button")}}** (as it is the button you need to press to open the drop-down menu).
> [!NOTE]
> The select `<button>` is {{glossary("inert")}} by default, to avoid any strange behavior caused by interactive children.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's inert because it's not meant to actually be exposed to the accessibility tree - the select and the button are meant to be treated as one element as far as all users are concerned. this note makes it seem like it'd be strange, but someone could put interactive children into the button part. i'd hope this could just be called out as invalid

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I've changed the note to the following, to hopefully clarify this:

The select <button> is {{glossary("inert")}} by default so that if interactive children (for example, links or buttons) are included inside it, it will still be treated like a single button for interaction purposes.

I don't think I need to mention the a11y tree here; when the "inert" link starts working (when that glossary page is published), that will explain exactly what the features of an inert element are, which includes not being exposed to the a11y tree.

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

- Plain old {{htmlelement("select")}}, {{htmlelement("option")}}, and {{htmlelement("optgroup")}} elements. These work just the same as in "classic" selects, except that they have additional permitted content types. The new features are built on top of existing select functionality as progressive enhancements, meaning that customizable selects can be designed to fall back to "classic" selects in non-supporting browsers.
- A {{htmlelement("button")}} element included as the first child inside the `<select>` element, which wasn't previously allowed in "classic" selects. When this is included, it replaces the default "button" rendering of the closed `<select>` element. You can then include arbitary content inside the `<button>`, to render whatever you want inside the closed `<select>`. This is comonly known as the **select {{htmlelement("button")}}** (as it is the button you need to press to open the drop-down menu).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are inconsistencies with referring to the drop-down.

drop-down picker
drop-down menu
picker drop-down menu
picker drop-down
drop-down options list

of the used variants, down-down picker is the most used, so probably should be used in place of some if not all of the variants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I've used "drop-down picker" throughout but abbreviated it to "picker" after the first mention in each section to lessen the wordage.

- The {{cssxref(":checked")}} pseudo-class, which targets the currently-selected `<option>` element.
- The {{cssxref("::checkmark")}} pseudo-element, which targets the checkmark placed inside the currently-selected `<option>` element to provide a visual indication of which one is selected.

In addition, the select `<button>` and the picker have the following behavior assigned to them automatically:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done via interactivity: inert, so authors can remove the inertness if they want

this sounds like a bad idea. why would that be allowed if it was made inert on purpose?

Copy link
Contributor

@scottaohara scottaohara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beyond my review comments - i wonder if it's not worth calling out that people can now use ::before and ::after with option elements to include other content - but none of that will be available if someone is using a browser that doesn't support customizable select.

additionally, while text content of an option may be used in lieu of a value - again the CSS content wouldn't participate in contributing to the submittable value - much like the alt text of an image won't participate in the value submitted.

This is nearly the same as "classic" `<select>` markup, with the following differences:

- The `<button><selectedcontent></selectedcontent></button>` structure represents the select {{htmlelement("button")}}; the {{htmlelement("selectedcontent")}} element contains a clone of the currently-selected {{htmlelement("option")}} (created using {{domxref("Node.cloneNode", "cloneNode()")}} under the hood). This allows you to select and [adjust the styling of the selected `<option>` contents as shown inside the select button](#adjusting_the_styling_of_the_selected_option_contents_inside_the_select_button). If this structure is not included in your markup, the browser will fall back to rendering the selected option's text inside the default button, and you won't be able to style it as easily.
- The rest of the `<select>` contents represents the picker (the `<select>` drop-down menu). This obviously includes the `<option>` elements representing the different choices in the picker, but you can also include other content if desired.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but you can also include other content if desired.

such as? is this referring to other allowed elements per the allowed content model - or is this unintentionally implying that disallowed content might be included? If so, then the parser won't kick it that content out, but the author will more than likely need to do additional work to make the a11y ux better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a bunch of testing to see what elements are allowed inside the picker besides the options; it seems to accept audio, video, images, text content, etc. I guess you could do some creative stuff with that. But it probably isn't a good idea. I've changed the bullet to:

The rest of the <select> contents represents the drop-down picker, which is usually limited to the <option> elements representing the different choices in the picker. You could include other arbitrary content in the picker, but it is not recommended.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better. And yes, the parser allows lots now. But the content model is limited https://whatpr.org/html/10586/dom.html#select-element-inner-content-elements-2

option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might it be worth calling out that if replacing the ::checkmark with other CSS content - that the content ideally should ideally be limited to representing a checked/selected state (like this example) - but that it also should be marked as decorative.

so for example, since this wasn't marked as decorative the checkmark used here is included in the accName for the option - making it announce with NVDA like "checkbox with check dog face dog" "checkbox with check" being the way that specific icon is exposed to the a11y tree.... that's not desired here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a note below this snippet:

When changing the icon like this, you should make sure to change it to something that will be announced as a checkmark (or similar) by assistive technologies so it makes sense to their users.

However, I'm not sure how to mark this as decorative, given that it's generated content on a pseudo-element, so there is no HTML to hang aria-hidden or alt="" (or whatever) on?

Copy link
Contributor

@scottaohara scottaohara Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSS content allows for declaring alt text.

Content: “foo” / “”;

would be the way to identify this content is decorative

option::checkmark {
order: 1;
margin-left: auto;
content: "☑️";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per previous comment - this should be marked as decorative. This accessibility consideration should be called out in this article as well, that if the replaced content is not marked as decorative, then it'll be included as part of the option's accessible name - and that is not recommended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note added here, plus same question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Content:CSS Cascading Style Sheets docs Content:HTML Hypertext Markup Language docs Content:Learn Learning area docs Content:WebAPI Web API docs size/xl [PR only] >1000 LoC changed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Customizable select element
3 participants