Add explainer: slotted options in customizable <select>#1357
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new explainer proposing that customizable <select> (appearance: base-select) should treat <option>, <optgroup>, and the trigger <button> as effective children when they are provided via slotting (including across nested components), enabling reusable select-based web components without JS cloning/workarounds.
Changes:
- Introduces a new explainer documenting the current limitation with slotted
<option>elements and why it matters for accessibility. - Specifies proposed behavior for slotted options/optgroups/trigger button, including nested slot scenarios and JS API expectations.
- Documents alternative approaches (JS rebuild, MutationObserver cloning, customized built-ins, etc.) and A11y/i18n/privacy/security considerations.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
anaskim
left a comment
There was a problem hiding this comment.
Overall, this seems like a good start to me. I wonder if we need to mention the issues that we could encounter if we implement Joey's solution: "I would solve this by changing the select element to look for options in its flat tree." Just so that we can point folks to a section in this doc if asked.
| component; this proposal doesn't let a shadow tree reach content it couldn't | ||
| already compose. | ||
|
|
||
| ## Stakeholder Feedback / Opposition |
There was a problem hiding this comment.
Do we also want to include browser's positions here? (Since I assume Chromium's should be positive)
There was a problem hiding this comment.
I don't know if we should do that now, as far as I know we request positions after having an explainer so we can reference it in the proposals, right?
kbabbitt
left a comment
There was a problem hiding this comment.
Great to see this! A few comments, mostly editorial.
| > **Note on scope:** This applies only to `<select>` in customizable mode | ||
| > (`appearance: base-select`). A `<select>` with the default native appearance is | ||
| > unchanged. |
There was a problem hiding this comment.
Why is default appearance out of scope? Couldn't I put a default appearance <select> inside a shadow DOM?
There was a problem hiding this comment.
It would be nice to have custom and non-custom select behave the same way for this. The biggest concern would be web compat. We might need use counters to see if this is a feasible change to make.
Maybe this should be an open question rather than something that's definitively scoped out?
There was a problem hiding this comment.
Yes, I was worried about web compat. Definitely open to more discussion about it and include it if necessary. @chrisdholt what are your thoughts?
There was a problem hiding this comment.
I think we actually lose the progressive enhancement story completely if this doesn't work generally for the non-customizable select. We can't fallback where it's not supported yet, we'd get an error in the console and it wouldn't render.
| > **Note on scope:** This applies only to `<select>` in customizable mode | ||
| > (`appearance: base-select`). A `<select>` with the default native appearance is | ||
| > unchanged. |
There was a problem hiding this comment.
It would be nice to have custom and non-custom select behave the same way for this. The biggest concern would be web compat. We might need use counters to see if this is a feasible change to make.
Maybe this should be an open question rather than something that's definitively scoped out?
| later, but are out of scope here. | ||
| - Letting arbitrary unrelated elements become a select's options. Only the | ||
| elements a `<select>` already accepts (`<option>`, `<optgroup>`, and the | ||
| trigger `<button>`) are in scope. |
There was a problem hiding this comment.
Does this mean the following won’t be possible?
<my-select>
<template shadowRootMode="open">
<select>
<slot></slot>
</select>
</template>
<my-option>
<template shadowRootMode="open">
<option><slot></slot></option>
</template>
option 1
</my-option>
<my-option>
<template shadowRootMode="open">
<option><slot></slot></option>
</template>
option 2
</my-option>
</my-select>This is mostly OK since a custom select element could still style the slotted in <option> elements with ::slotted(option), but there are a couple issues:
-
This could make a component library slightly inconsistent, in that most of the components may ask users to only slot in other custom elements, but the custom select asks for native
<option>elements. -
If we consider the use case with
<optgroup>, e.g.<my-select> <template shadowrootmode="open"> <select> <slot></slot> </select> </template> <optgroup> <option>One</option> <option>Two</option> </optgroup> </my-select>
Currently there’s no way to style the
<option>elements within<my-select>’s shadow root, because::slotted()can only reach the<optgroup>element. So developers may still want/need to encapsulate option’s styles in a custom option component.
Adds a new
SlottedOption/explainer proposing that a customizable<select>(appearance: base-select) recognize<option>,<optgroup>, and its trigger<button>when they are slotted in from a web component, so they behave as if they were direct children of the<select>.Today a
<select>only treats its direct DOM children as options. If you put a<select>in a component's shadow root and expose a<slot>, the slotted options are ignored and the dropdown is empty. The only options today are to sync optionswith a
MutationObserveror rebuild the control by hand, which is a lot of code and is usually less accessible than the native control.Discussion in WHATWG HTML issue: whatwg/html#11535