Hacker News new | past | comments | ask | show | jobs | submit login
How should the new <selectedoption> element work? (jakearchibald.com)
65 points by jaffathecake 3 months ago | hide | past | favorite | 48 comments



IMO, this whole cloning idea is overcomplicated. Let the duplication of markup happen server side or in React (or whatever).

If the developer wants a <selectedoption>, they should fill it with <option> elements that correspond to the <option> elements that appear in the dropdown. Each option could reuse the <label for=""> pattern to associate them with their counterpart.

Then, when an option is selected by the end user, the corresponding option inside the <selectedoption> would get a `selected` attribute.

The default CSS would be:

selectedoption > option { display: none; } selectedoption > option[selected] { display: block; }

This gives complete control to the developer, works fine without JS, and wouldn't have shocking side effects.

As they say, "duplication is cheaper than the wrong abstraction".


I think it is worthwhile to revisit the starting motivation of customizing `<select>`. There would be generally two camps:

1. Declarative camp, where people expect two differently stylable copies of `<option>` somewhere in `<select>`. I don't think the exact copying mechanism and timing is very important here, as long as it can be safely done and works without JS.

2. Programmable camp, where people want the full control over appearance and behavior. We can reasonably assume that JS is required for this camp, and they generally have better understanding of what gets rendered or not in the first place.

Given both requirements, it seems wise to couple the clone timing with the event processing, which most cases in the second camp would have to do anyway. In the other words, the `input` event would have the default behavior of full cloning, and that default behavior should be prevented via `e.preventDefault()`. The second camp will almost always use this option and can use their own `MutationObserver` or similar to implement edge cases described in the OP. The timing and extent for the default behavior can be then optimized without affecting the second camp. It is even possible to distinguish two camps via different values of the `appearance` CSS property.

Sidetrack: It seems that `<selectedoption>` indeed exists mainly to distinguish two camps? But that leaves many edge cases like multiple `<selectedoption>` elements I believe. Maybe something like `<select reflect="foo"><button><option id="foo">...</option></button>...</select>` might be more appropriate. (`<output>` was my initial thought, but `<option>` would be more regular since it can't nest.)


> the `input` event would have the default behavior of full cloning

Hm, it feels like, when the selected option is changed via user interaction, the <selectedoption> should be populated before the event fires.

But this is really besides the point, because not all selected-option-changes come with an input event, eg programatic change, including to the content of the currently selected option, which the post focuses on.

> that leaves many edge cases like multiple `<selectedoption>` elements I believe

That isn't really a big problem. It's something the platform already deals with all over the place, like multiple <title> elements, multiple elements with the same ID etc etc.


Correct if I'm wrong, but this still doesn't allow us to define the dropdown content and selected content for each option separately? Maybe something like this:

    <select name="commenters">
        <option value="annevk">Anne van Kesteren</option>
        <option value="jakearchibald">
            <label>Jake Archibald</label>
            <selectedcontent>
                <div>
                    <img src="profile.avif" alt="Jake's photo">
                    <div>Jake Archibald<br><small>@jakearchibald</small></div>
                </div>
            </selectedcontent>
        </option>
        <option value="sorvell">Steve Orvell</option>
    </select>

The value attribute would be required when using these new sub elements. This structure feels familiar and progressive to me.

Naively, I would imagine that the following JavaScript would cause 1 DOM update, 1 redraw of the option if the dropdown is open, and 1 redraw of the selected option:

    document.querySelector('option:selected selectedcontent').innerHTML = 'Jake Archibald';
Obviously, things are different when using multiple. Maybe a `select > selectedcontent` element containing a copy of each `option > selectedcontent` element that is updated on change events.


> Correct if I'm wrong, but this still doesn't allow us to define the dropdown content and selected content for each option separately?

There are a few different cases worth considering:

# I want to display the option content in the <selectedoption> as is

Great! Use <selectedoption>

# I want to display the option content in the <selectedoption> a little differently (eg, hide the associated <img> or a different font-weight)

In this case, you're probably best off using a little CSS to style the content of <selectedoption> different, eg hide particular elements.

# I want to display something completely different in the <selectedoption>

<selectedoption> is optional, so one thing you can do here is just… not use it, and instead change the content of your <button> in response to state changes. This requires JavaScript.

If you really want to use <selectedoption> for this, then you could do this:

    <option>
      <div class="option-content">…</div>
      <div class="selectedoption-content">…</div>
    </option>
Along with the CSS:

    option .selectedoption-content { display: none }
    selectedoption .option-content { display: none }


I think it should be Option 2, "Automatically reset the content when anything in the selected <option> changes."

But furthermore, I think it should be possible to turn <selectedoption> mirroring entirely off, e.g. with an attribute like <selectedoption mirroring="none">, and I think most developers using reactive frameworks should prefer to do that.

If I'm using any reactive framework designed to do targeted DOM updates, I want my framework to be in complete control. When an option is selected, I'll populate <selectedoption> myself. When an <option> is modified, I modified it, and I know which <option> is selected, so I'll perform a targeted DOM update of <selectedoption> as well.

You had a whole separate podcast episode about how/why having the browser itself do targeted DOM updates is an enormous can of worms, mostly for attribute vs. property reasons. https://offthemainthread.tech/episode/putting-react-in-the-b... And anyway, every framework wants to handle mutations differently, and framework designers are in consensus that we haven't picked a clear winner yet. So, as nice as it would be if we had a replaceHTML API that everybody loved, we don't have that now, and we shouldn't hold up customizable <select> for this.

<selectedoption> mirroring is for folks who don't want to use any JavaScript (and I think that's a good thing). In that case, mirroring all updates automatically, synchronously, is the simplest thing that can possibly work.

Developers who want optimal performance want full control, and they just want to turn mirroring entirely off.


Thanks for the feedback! Fwiw, there's no point populating <selectedoption> yourself. Just don't use <selectedoption>. It's optional. That's probably what I'd do in most cases where I'm using a framework.


Good to know.

Say, is there a bug in Chrome Canary 132? When I opt into customizable <select>, "change" events stop firing.

    <style> select, ::picker(select) { appearance: base-select; } </style>
    <select>
    <option value="one">one</option>
    <option value="two">two</option>
    <option value="three">three</option>
    </select>
    <script>
    document.querySelector('select').addEventListener('change', e => {
        console.log(e);
    })
    </script>
And another. When I add <selectedoption>, I get a warning in the console, "A descendant of a <select> does not follow the content model. <selectedoption> one </selectedoption> "

    <style> select, ::picker(select) { appearance: base-select; } </style>
    <select>
    <button><selectedoption></selectedoption></button>
    <option value="one">one</option>
    <option value="two">two</option>
    <option value="three">three</option>
    </select>


Those sound like bugs but I'm not really familiar with the implementation. I haven't played with it much either. I'm just an outsider interested in the feature design.


Why should styling even have a separate element? And duplicated? I thought the whole point of CSS was to not clutter HTML with styling.

This shouldn't be a literal new element. It should be a psuedo-element like other component parts like `::-[engine]-slider-thumb` are. You can then style `select::selected-option` and not have to mangle your HTML or worry about mirroring HTML.


The issue is that the "selected <option>" and "the <selectedoption> in the <button>" need to be able to render at the same time, behave independently, and be independently styled.

If you have a psuedo-element, it still needs to get its content from somewhere, and needs to be placed somewhere in the tree. With <selectedoption> you can put it wherever you want in the <button>, but you'd lose that functionality with a pseudo. The same questions remain about when the content of the pseudo gets updated. And now you need to figure out how events work.


I agree the question of events still kind of remains. I think they should always be mirrored because they shouldn't be two separate elements. You shouldn't be able to change the "selectedoption" because it should be a pseudo-element. If the <option> changes, the pseudo-element should immediately change. It gets its content from the option.

You don't need to rearrange the HTML of the "button". You can use ::before, ::after, and general styling to do anything you need to. I'm struggling to see any advantage even if you were okay with using HTML to style pages.


> I think they should always be mirrored because they shouldn't be two separate elements

That maintains one of the problems I'm worried about. If you have a mouseenter listener on something in an <option> that triggers a fancy animation, you (surely most of the time) don't want that action repeated in the <selectedoption>, because it's the thing in the <option> that was hovered, not the thing in the <selectedoption>.

> You can use ::before, ::after, and general styling to do anything you need to.

You don't need to look at too many web sites to see examples where folks have added divs for styling, because pseudo-elements didn't provide enough style hooks.


You can always add <div>s to the <option> itself and conditionally check whether it's `select > option` or `select::selected-option`.


I would lean towards option 1, because it's the only one which doesn't lock the other options out.

You can just not provide selectedoption or provide it in a limited way, and see what patterns emerge in how people use it in practice. Maybe it'll turn out that people want something completely different from selectedoption's current design. Maybe it'll turn out that people often want to display something different between the select menu and the select button and selectedoption doesn't actually bring much to the table.


My feeling is that the most important situation to consider here is where the updating happens from third-party JS. If you're writing your own JS, it isn't that much harder to just target the selectedoption as well as the original one, and if you're using a full-fat framework like React or whatever it's downright easy. So I think the benefit of providing an explicit API to trigger a clone is limited.

So that leaves the various automatic options, either synchronous, debounced, or the fancy targeted version. This seems like a pretty straightforward complexity/performance tradeoff to me, with the synchronous version being the simplest (both to implement and to understand) and going up from there.

With that in mind, I'm inclined toward the middle option (changes are synced automatically, but batched in the next microtask) since it feels like the best balance of complexity/usability. Seems like it would eliminate some of the biggest performance footguns, without being too much of a bear to implement or too difficult to understand.

On the other hand, I would personally also be ok with no syncing at all, just the initial clone when an option is selected, if it would mean we get this feature sooner. Really looking forward to not having to roll my own dropdowns.


> With that in mind, I'm inclined toward the middle option (changes are synced automatically, but batched in the next microtask)

fwiw, this is relatively easy to do yourself. Set up a mutation observer, and clone when the callback fires on a mutation within the selected option.


If you're using a framework like React to manage updates to the DOM, then you probably don't want to use selectedoption in the first place because it's just as easy and more flexible/predictable to use the framework's existing patterns for keeping different parts of the DOM in sync.

So the main target audience for selectedoption is probably people not using a framework, and using no or little JavaScript. That audience probably would want selectedoption to "just work" without having to manually call a function to reset things. So while option 1 seems like a reasonable option to me personally as someone who mainly works with React, it's probably not what most people using selectedoption would want.

Option 2 just has too much potential for introducing hard to debug and hard to fix performance issues, so I feel like it's automatically not the best choice. Its main advantage is that it's probably the most straightforward to understand/explain.

Option 4 would help with the performance issues, but it sounds like it would be a nightmare for browsers to implement. It would also be a bit difficult to explain to devs how exactly it works and what edge cases need to be considered (like mutating the selectedoption "fork" potentially causing issues).

So by process of elimination option 3 seems like the best to me. It solves the performance issues but it's still pretty easy to understand/explain. It's main disadvantage seems to be that from the perspective of code running synchronously it's possible for the selectedoption and option to get out of sync, but 1) it's a bit hard to imagine a practical use case for needing to do this kind of comparison and 2) simply waiting until the next microtask before doing the comparison would probably be an easy fix even if this is a problem in some cases.

For all of these options, it might be useful for there to be some way to intercept the default behavior. For example maybe there could be some event such that calling .preventDefault() inside an event handler has the side effect of preventing the selectedoption from being reset, if resetting it is not desired. Of course you don't need to use selectedoption at all if the automatic resetting is not desired, but maybe there will be some cases where you only want to make an exception in a few cases but generally do want the selectedoption default behavior.


> Option: Nothing by default, but provide a way to trigger an update

I would strongly suggest this option unless there's a strong reason the browser must keep the elements in sync automatically.

Unless I'm mistaken, the other options could all be done in userland with a MutationObserver, a debounce method wrapping a timeout, and custom logic optimizing updates for specific attributes or changes.

I'm particularly worried about how automatic updates/cloning will work across different frontend frameworks. Today's more popular frameworks already diverge quite a bit with how they handle rerendering and DOM updates, and the next big framework to come along could take a more radical approach to rendering and interactivity.

In my opinion, anything that can reasonably be done in user code should be done there - at least until it causes enough problems that it needs to be solidified into browser specs. I'm always worried that the any clever built-in magic may work today but cause problems later, and because removing the spec is difficult most users would just skip the new <selectedoption> element all together.


Is this a path towards a fully style-able select element that still operates correctly with keyboard and accessibility features?

It never fails to amaze me how many bad semi functional “select” controls I encounter on a regular basis. I must be part of the small minority that fills out forms entirely with the keyboard using the tab key and prefix matching.


> Is this a path towards a fully style-able select element that still operates correctly with keyboard and accessibility features?

That's one of the main driving forces behind this effort - to allow developers to style <select> without failing in terms of keyboard and accessibility.

However, if you choose to make a <select> where the options are horizontal, or circular, you will have to add the 'different' keyboard interactions yourself. Up and down arrow keys will still cycle between options, but if your options are horizontal left & right keys would make more sense.


Is <button> a valid child of <select> now? How do we deal with an "interact:able" element nested inside another such element gracefully? (i.e. not pushing a lot of preventDefaults and stopping event bubbling here and there).


It's valid in this latest proposal. It becomes the item displayed on the page rather than the browser-controlled 'button'.

In browsers that don't support customisable select, the button is ignored.

https://codepen.io/jaffathecake/pen/NWQaxmG?editors=1100 - compare this in Chrome Canary with chrome://flags/#enable-experimental-web-platform-features vs other browsers (or the flag disabled).


All I can find is a Chrome experiment. Do you have a draft spec available?

Edit: Found a link to the Chrome blog which in turn links to https://github.com/whatwg/html/issues/9799

Initial question remains however, how are we to properly distinguish the user interface actions?


What does "distinguish the user interface actions" mean here?

The browser knows the <button> that's a child of <select> is special, and activates the popover on click. This is handled automatically, as is the accessibility tree.


If it is special, why not use a new element and not one with a history and a clear use today? From my experience it seems to muddle the concept of the existing element. An <a> for example never behaves differently depending on if it is part of a (DOM) tree with a <div> or a <p> as a parent/grandparent.


It does. An <a> doesn't work as usual when it's within a <button> for instance.

<button> already behaves differently depending on the type of form it's in, or whether it's in a form at all.

Let me put it another way: If you use a <select> today, what's the difference between the thing that renders on the page and a <button>? They look different, sure, but that's a matter of styling. You can already style a <button> to look like a <select> activator.

They can receive focus, and they can be activated. They're buttons.


Adding another gotcha will help no-one.

This line of argument reminds me of Plat's "featherless bipeds" as a definition of man. Everything's a button if you are ambiguous enough, sure. It's not _wrong_ but it also dilutes things to where semantic meaning no longer exists.


IMO, the extra functionality this gives does not warrant the complexity, extra cognitive load and learning curve for new developers.

Existing solutions solve enough use cases, and the remaining use cases can use javascript to build whatever they like.


It looks like the proposal is to build a little single purpose reactive UI engine into <select><button><selectedcontent>, and we're talking over the details.

To me it's a bad idea from the top.

Either leave reactive updates to frameworks, or develop a general built-in reactive framework. Building a one-off that introduces an all-new pattern into the browser seems like a perpetual headache.


Wait, you're proposing that <input type="text"> shouldn't work when you type into it by default, since that's a reactive UI and should be done with a framework?

Wait, you're proposing that CSS shouldn't be able to define hover effects for links, since that's a reactive UI and should be done with a framework?

Wait, you're proposing that text selection shouldn't work by default, since that's a reactive UI and should be done with a framework?

etc etc

There's already a "built-in reactive framework" for these things.


The selectoption element is on another level of reactivity. It can both detect and produce (synchronous) changes to the DOM.

That's a massive can of worms that no web standard has yet opened. Snark all you like, it's reasonable to think we shouldn't open that can so casually.


I agree it's new and different, but I disagree that it needs a whole new framework.

Whether it's synchronous or not is part of the debate.

There are already things like SVG's <use> element that 'clone' content, although it's not in the light DOM.

There's definitely stuff to debate here, but "this is bad. 'Reactive' should be left to frameworks" is not useful.


> Wait, you're proposing...

No. Some significant differences between those examples and the new proposal:

(1) (most important) Those are all implemented and released. The time to debate how those things might best be handled is long over.

(2) The interactivity of text editing and selection are limited to local state of the control itself. Notably, they don't affect the DOM. pseudo-classes like :hover are effectively DOM operations, but are at least limited to a kind of boolean state flag, which is a lot less complicated and fraught than rendering arbitrary DOM subtrees.

Anyway, as I already mentioned, I'm not arguing against a reactive mechanism built in to the browser. I'm saying if you do it, it should not be a one-off for a single, specific case like <select><button><selectedcontent>, but should instead be a generally applicable mechanism.


I think the main issue is you're using "reactive" to mean something specific that you're not stating. The web is always going to have the DOM as its tree, so a system where one thing updates in reaction to another thing updating is always going to need to be explained in terms of DOM mutations, and their timings. The post details multiple ways of doing that, all of which are reactive by some definition. So, if you're saying it needs to be "reactive", it isn't of much value unless you detail how that works and how it's different to what's proposed.


This seems overcomplicated. Maybe I'm missing something, but is there some reason not just to define a psuedoelement on the current option?

  option::picker-current { ... }


The aim of <selectedoption> is to provide a DOM placeholder to contain a clone of the contents of the selected <option>. It isn't there as a style hook for the selected option in the popover - that already exists via option:selected.

You aren't the first to get mixed up here. Personally I think <selectedoption> is a misleading name. I wish it was called something like <selectedcontent>, but I don't know if that is much better https://github.com/openui/open-ui/issues/1112


selection?


Generally means "selected text". I've pitched <selectedcontent>, since it's the content of the selected option.


First option is clearly best just because it lets developers decide. Every select library should have a method like this and I breathe a sigh of relief every time I find one.


Better add a <optiontitle> element


Someone tell me I'm wrong and why, but this whole thing seems like unnecessary complication? We have JavaScript for bespoke select tags.


See https://news.ycombinator.com/item?id=41912960 - the custom ones are almost universally bad.

https://extensiblewebmanifesto.org/ - I think this is a good template for how the platform should grow. Folks defining the platform should observe what web developers do, and come up with features that make that work better, faster, and more reliable.

In this example, folks historically have recreated their own <select> from scratch because <select> isn't customisable, and in doing so they create an experience with poor accessibility and keyboard navigation.

This effort lets them take native accessibility and keyboard navigation, whilst being able to control the visual style of the <select> button and its popover.

You can still use JavaScript with your customised <select>, but you don't need as much.

Here's a podcast where I go through 10 related general features we got along the way to making this work https://offthemainthread.tech/episode/stylable-select-elemen....


Writing your own select menu means having to redo all the accessibility built into the <select> element. Keyboard accessibility seems to be particularly bad for a lot of custom select menus I come across.


We have endless different poorly-implemented variations that sort of look like <select> but typically lack the semantics or non-pointer operability they require.

Also, the JS variations are less performant and often insufficiently responsive to viewport/container sizes (not that a highly stylable <select> will be immune to layout problems).


Who is in charge of these HTML changes? This is horrible. It’s bad enough that for the last three decades we’ve have to use the `selected` attribute, now we are going to have a `button` in `select` with a `selectedoption` tag?

Please, just No!

The CSS for the style of the element belongs with the `select` tag. And all that should be needed to demark the selected option is a matching `value` attribute.

I get the sense that the people currently working on HTML and CSS are front-end designers and not programmers. We need functional design, polymorphism and simplicity, not just more kitchen sinks.


> now we are going to have a `button` in `select` with a `selectedoption` tag?

Can you detail why this is such an awful thing?

> The CSS for the style of the element belongs with the `select` tag

That isn't even true today. A select is made up of multiple parts, the button that's always visible on the page, and the resulting menu/popover which contains multiple options. These can already be styled a bit depending on the platform.

However, folks want to style them in more detail, and as a result they end up having to recreate a select from scratch, and in almost all cases they mishandle the keyboard navigation and accessibility. The aim here is to provide the freedom of styling whilst providing good keyboard and accessibility out of the box.

> I get the sense that the people currently working on HTML and CSS are front-end designers and not programmers.

I'm sorry to tell you that your sense is way off. The strongest voices in web standards are browser engineers, who tend to be more familiar with C++ than building web apps/sites.

This is why we need to ensure that web developer voices are heard in these discussions, hence the post.


> Who is in charge of these HTML changes? This is horrible.

We all are, so thank you for this horrible proposed-but-not-yet-implemented change!

Jokes aside, you can participate in the discussion yourself here: https://github.com/whatwg/html/issues/10520

But be warned, you will probably not be able to convince people with just "Please, just no!".

> I get the sense that the people currently working on HTML and CSS are front-end designers and not programmers.

Yeah, that makes sense, it's a markup language, not a programming language. The targeted audience for HTML and CSS is way bigger than just programmers. For the people who want to build imperative UIs or even "immediate mode" GUIs there are a ton of options out there already.

I get the sense that people complaining about HTML/CSS not being like a programming language enough are programmers, not document authors.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: