Skip to content

Add explainer: slotted options in customizable <select>#1357

Open
ffiori wants to merge 4 commits into
MicrosoftEdge:mainfrom
ffiori:slotted-option
Open

Add explainer: slotted options in customizable <select>#1357
ffiori wants to merge 4 commits into
MicrosoftEdge:mainfrom
ffiori:slotted-option

Conversation

@ffiori

@ffiori ffiori commented Jul 6, 2026

Copy link
Copy Markdown
Member

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 options
with a MutationObserver or 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

@ffiori ffiori marked this pull request as ready for review July 6, 2026 23:11
@ffiori ffiori requested a review from Copilot July 6, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
@ffiori ffiori requested review from anaskim, dandclark and leotlee July 6, 2026 23:21

@anaskim anaskim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread SlottedOption/explainer.md
component; this proposal doesn't let a shadow tree reach content it couldn't
already compose.

## Stakeholder Feedback / Opposition

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we also want to include browser's positions here? (Since I assume Chromium's should be positive)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 kbabbitt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great to see this! A few comments, mostly editorial.

Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
Comment on lines +95 to +97
> **Note on scope:** This applies only to `<select>` in customizable mode
> (`appearance: base-select`). A `<select>` with the default native appearance is
> unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is default appearance out of scope? Couldn't I put a default appearance <select> inside a shadow DOM?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I was worried about web compat. Definitely open to more discussion about it and include it if necessary. @chrisdholt what are your thoughts?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated

@dandclark dandclark left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a good explainer!

Comment thread SlottedOption/explainer.md
Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
Comment on lines +95 to +97
> **Note on scope:** This applies only to `<select>` in customizable mode
> (`appearance: base-select`). A `<select>` with the default native appearance is
> unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Comment thread SlottedOption/explainer.md
Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
Comment thread SlottedOption/explainer.md Outdated
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. 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.

  2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants