Say Hello to selectmenu, a Fully Style-able select Element

I want to introduce you to a new, experimental form control called <selectmenu>. We’ll get deep into it, including how much easier it is to style than a traditional <select> element. But first, let’s fill in some context about why something like <selectmenu> is needed in the first place, as it’s still evolving and in development.

An animated screenshot showing a selectmenu element with emojis as options against a bright teal background.

Ask any web developer what they think is missing from the web platform today, chances are the ability to style form controls will be on their list. In fact, form styling was voted as one of the top-10 missing things in the State of CSS Survey in 2020. It was then further surveyed by Greg Whitworth who showed that <select> was the control web developers were having the most problems styling with CSS.

While it’s relatively easy to style the appearance of the button part of a <select> (the thing you see in the page when the popup is closed), it’s almost impossible to style the options (the thing you see when the popup is open), let alone add more content within the popup.

Showing the default UI of the select element in Safari.
The default UI for a <select> element in Safari

As a result, design systems and component libraries have been rolling out their own selects, made from scratch using custom HTML markup, CSS, and often a lot of JavaScript, in order to have something that integrates nicely with the other components.

Unfortunately, doing so correctly with the right accessibility semantics, keyboard support, and popup positioning is not easy. Web developers have poured hours and hours over the years, trying to solve the same problems over and over, and there are many inaccessible selects out there.

It’s about time we got a properly style-able built-in <select> so we don’t have to write this code ever again!

The Open UI initiative

The Open UI logo, which is a green oval with a rounded fork-like shape with three prongs inside.

Open UI is a group of developers, designers, and browser implementers who set out to solve this exact problem, and while they’re at it, tackle other missing controls too.

The purpose of Open UI is to eventually make it possible for web developers to style and extend built-in UI controls (this includes <select>, but dropdowns, checkboxes, radio buttons, and others too). To achieve this, they produce specifications for how these controls should be implemented in the web platform as well as the accessibility requirements they should address.

The project is still in its infancy, but things are moving fast and, as we’ll see below, exciting things are already happening.

You can join the group and participate in the meetings, research, and specification efforts.

The <selectmenu> control

Based on the Open UI’s <select> proposal, the implementation of a new <selectmenu> control has started in Chromium! The work is done by the Microsoft Edge team, in collaboration with the Google Chrome team. It’s even already available in Chromium-based browsers by enabling the “Experimental Web Platform features” flag in the about:flags page.

<selectmenu> is a new built-in control that provides an option selection user experience, just like <select>, with a button showing the selected value label, a popup that appears when that button is clicked, and a list of options that get displayed.

Why a new name?

Why not just replace the existing <select> control? The name “selectmenu” started as a working name, but it seems to have stuck so far, and no one has come up with anything better yet.

More importantly, the existing <select> control has been used on the web for a very long time. As such, it can probably never be changed in any significant way without causing major compatibility issues.

So, the plan (and remember this is all still very experimental) is for <selectmenu> to be a new control, independent from <select>.

Try it out today

This isn’t ready for production use yet, but if you’re as excited as I am about using it, here’s how:

  1. Open a Canary version of a Chromium-based browser (Chrome, Edge).
  2. Switch the “Experimental Web Platform features” flag in the about:flags page and restart.
  3. Replace any <select> by <selectmenu> in a web page!

That’s it! It won’t do much by default, but as we’ll see later, you’ll be able to style and extend the control quite extensively with this one tag name change.

We love feedback!

Before we go into how to use the control, if you do use it, the Open UI group and people working on the implementation in Chromium would love to hear your feedback if you have any.

By being an early tester, you can actively help them make the control better for everyone. So, if you encounter bugs or limitations with the design of the control, please send your feedback by creating an issue on the Open UI GitHub repository!

And now, let’s talk about how the control works.

The anatomy of a <selectmenu> control

Because the various parts of the selectmenu can be styled, it’s important to first understand its internal anatomy.

Showing the boundaries of a selectmenu element.
  • <selectmenu> is the root element that contains the button and listbox.
  • <button> is the element that triggers the visibility of the listbox.
  • <selected-value> is the element that displays the value of the currently selection option (optional). Note that this part does not necessarily have to be placed inside the <button> part.
  • <listbox> is the wrapper that contains the <option>s and <optgroup>s.
  • <optgroup> groups s together with an optional label.
  • <option> represents the potential value that can be chosen by the user. There can be one or more.

Default behavior

The default behavior of the <selectmenu> control mimics the behavior of the <select> control. You can use it just like a native <select>, with the following minimal markup.

<selectmenu>
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</selectmenu>

When doing so, the default <button>, <selected-value>, and <listbox >are created for you.

Styling parts of the control

This is where things become interesting! One way to style the control to match your requirements is to use the CSS ::part() pseudo-element to select the different parts within the control’s anatomy that you wish to style.

Consider the following example where ::part() is used to style the button and the listbox parts:

<style>
  .my-select-menu::part(button) {
    color: white;
    background-color: #f00;
    padding: 5px;
    border-radius: 5px;
  }

  .my-select-menu::part(listbox) {
    padding: 10px;
    margin-top: 5px;
    border: 1px solid red;
    border-radius: 5px;
  }
</style>
<selectmenu class="my-select-menu">
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</selectmenu>

The above example results in the following style:

A styled selectmenu element with a red button background and a red border around the listbox.

::part() can be used to style the <button>, <selected-value>, and <listbox> parts of the control.

Use your own markup

If the above isn’t enough for your needs, you can customize the control much more by providing your own markup to replace the default one, and extend or re-order the parts.

A <selectmenu> has named slots that can be referenced to replace the default parts. For example, to replace the default button with your own, you can do the following:

<style>
  .my-custom-select [slot='button'] {
    display: flex;
    align-content: center;
  }
  .my-custom-select button {
    padding: 5px;
    border: none;
    background: #f06;
    border-radius: 5px 0 0 5px;
    color: white;
    font-weight: bold;
  }
  .my-custom-select .label {
    padding: 5px;
    border: 1px solid #f06;
    border-radius: 0 5px 5px 0;
  }
</style>
<selectmenu class="my-custom-select">
  <div slot="button">
    <button behavior="button">Open</button>
    <span class="label">Choose an option</span>
  </div>
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</selectmenu>

The slot="button" attribute on the outer <div> tells the <selectmenu> to replace its default button with the contents of the <div>.

The behavior="button" attribute on the inner <button> tells the browser that this element is what we want to use as the new button. The browser will automatically apply all the click and keyboard handling behavior to this element as well as the appropriate accessibility semantics.

The above code snippet results in the following style:

A styled selectmenu with a bright pink open button and a box-shadow around the listbox.

Note that the slot and behavior attributes can also be used on the same element.

You can replace the default listbox part in a similar fashion:

<style>
  .my-custom-select popup {
    width: 300px;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
    gap: 10px;
    padding: 10px;
    box-shadow: none;
    margin: 10px 0;
    border: 1px solid;
    background: #f7f7f7;
  }
</style>
<selectmenu class="my-custom-select">
  <div slot="listbox">
    <popup behavior="listbox">
      <option>Option 1</option>
      <option>Option 2</option>
      <option>Option 3</option>
      <option>Option 4</option>
      <option>Option 5</option>
    </popup>
  </div>
</selectmenu>

Interestingly, the <popup> used here is also being proposed by Open UI and implemented in Chromium at the moment.

The element with behavior="listbox" is required to be a <popup>. Applying behavior="listbox" tells the browser to open this element when the <selectmenu> button is clicked, and the user can select <option>s inside it with mouse, arrow keys, and touch.

The above code snippet results in the following style:

A styled selectmenu where the list box is split into two columns.

Extending the markup

Not only can you replace the default parts with your own, as seen above, you can also extend the control’s markup by adding new elements. This can be useful to augment the listbox or button with extra information, or to add new functionality.

Consider the following example:

<style>
  .my-custom-select [slot='button'] {
    display: flex;
    align-items: center;
    gap: 1rem;
  }
  .my-custom-select button {
    border: none;
    margin: 0;
    padding: 0;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    display: grid;
    place-content: center;
  }
  .my-custom-select button::before {
    content: '\25BC';
  }
  .my-custom-select popup {
    padding: 0;
  }
  .my-custom-select .section {
    padding: 1rem 0 0;
    background: radial-gradient(ellipse 60% 50px at center top, #000a 0%, transparent 130%);
  }
  .my-custom-select h3 {
    margin: 0 0 1rem 0;
    text-align: center;
    color: white;
  }
  .my-custom-select option {
    text-align: center;
    padding: 0.5rem;
  }
</style>
<selectmenu class="my-custom-select">
  <div slot="button">
    <span class="label">Choose a plant</span>
    <span behavior="selected-value" slot="selected-value"></span>
    <button behavior="button"></button>
  </div>
  <div slot="listbox">
    <popup behavior="listbox">
      <div class="section">
        <h3>Flowers</h3>
        <option>Rose</option>
        <option>Lily</option>
        <option>Orchid</option>
        <option>Tulip</option>
      </div>
      <div class="section">
        <h3>Trees</h3>
        <option>Weeping willow</option>
        <option>Dragon tree</option>
        <option>Giant sequoia</option>
      </div>
    </popup>
  </div>
</selectmenu>

Here we’re using custom markup to wrap the list of options and create our own content as seen below:

A styled selectmenu that contains options containing sub-options in the listbox.

Replacing the entire shadow DOM

Finally, and if the above wasn’t enough, you can also extend the control’s markup by replacing its default shadow DOM altogether by calling attachShadow(). For example, the demo in the previous section could be modified as follows:

<selectmenu id="my-custom-select"></selectmenu>
<script>
  const myCustomSelect = document.querySelector('#my-custom-select')
  const shadow = myCustomSelect.attachShadow({ mode: 'closed' })
  shadow.innerHTML = `
    <style>
    .button-container {
      display: flex;
      align-items: center;
      gap: 1rem;
    }
    button {
      border: none;
      margin: 0;
      padding: 0;
      width: 2rem;
      height: 2rem;
      border-radius: 50%;
      display: grid;
      place-content: center;
    }
    button::before {
      content: '\\0025BC';
    }
    popup {
      padding: 0;
    }
    .section {
      padding: 1rem 0 0;
      background: radial-gradient(ellipse 60% 50px at center top, #000a 0%, transparent 130%);
    }
    h3 {
      margin: 0 0 1rem 0;
      text-align: center;
      color: white;
    }
    option {
      text-align: center;
      padding: 0.5rem;
    }
    option:hover {
      background-color: lightgrey;
    }
  </style>
  <div class="button-container">
    <span class="label">Choose a plant</span>
    <span behavior="selected-value" slot="selected-value"></span>
    <button behavior="button"></button>
  </div>
  <popup behavior="listbox">
    <div class="section">
      <h3>Flowers</h3>
      <option>Rose</option>
      <option>Lily</option>
      <option>Orchid</option>
      <option>Tulip</option>
    </div>
    <div class="section">
      <h3>Trees</h3>
      <option>Weeping willow</option>
      <option>Dragon tree</option>
      <option>Giant sequoia</option>
    </div>
  </popup>
  `
</script>

Written this way, the <selectmenu>‘s custom markup is fully encapsulated in its shadow DOM. The <selectmenu> can therefore be dropped into any page without risk of interference from the surrounding content’s styles.

Closing remarks

As we’ve seen, the new experimental <selectmenu> control offers a lot of flexibility when it comes to styling and even extending a traditional <select>. And it does this in all the right ways, because it’s built into the browser where accessibility and viewport-aware positioning are handled for you.

Open UI has more documentation about <selectmenu>, and if you want to see more code showing how to use the <selectmenu>, here are a few demos as well.

Again, this is work in progress and will most certainly change as a result of feedback received by the Open UI group.

I can’t wait to see specifications start to appear in HTML and CSS standard bodies, and for the implementation to become more stable, as well as see other browser engines getting interested in this. You can help make this happen! Testing the control, reporting issues, or getting involved are all great ways to help push this effort forward.


Say Hello to selectmenu, a Fully Style-able select Element originally published on CSS-Tricks. You should get the newsletter.

CSS Custom Highlight API: The Future of Highlighting Text Ranges on the Web

Styling ranges of text in software is a very useful thing to be able to do. Thankfully, we have the CSS Custom Highlight API to look forward to because it represents the future of styling text ranges on the web.

Animation screenshot of the CSS Custom Highlight API demo.

One example: if you’ve ever used text editing software like Google Docs, Word, or Dropbox Paper, you’ll see they detect spelling and grammar errors and displaying nice little squiggly underlines below them to attract attention. Code editors like VS Code do the same for code errors.

Another very common use case for highlighting text is search and highlight, where you’re given a text input box and typing in it searches matching results on the page, and highlights them. Try pressing Ctrl/+ F in your web browser right now and type in some text from this article.

The browser itself often handles these styling situations. Editable areas (like a <textarea>) get spelling squiggles automatically. The find command highlights found text automatically.

But what about when we want to do this type of styling ourselves? Doing this on the web has been a common problem for a long time. It has probably costed many people a lot more time than it should have.

This isn’t a simple problem to solve. We aren’t just wrapping text in a <span> with a class and applying some CSS. Indeed, this requires being able to correctly highlight multiple ranges of text across an arbitrarily complex DOM tree, and possibly crossing the boundaries of DOM elements.

There are two common solutions to this, including:

  1. styling text range pseudo-elements, and
  2. creating your own text highlighting system.

We’ll review them first and then take a look at the upcoming CSS Custom Highlight API that can change it all. but if you’re

Potential Solution #1: Style-able Text Ranges

Probably the most well-known style-able text range is the user selection. When you use your pointing device to select a piece of text in a web page, a Selection object is automatically created. In fact, try selecting text on this page right now, and then run document.getSelection() in the DevTools console. You should see location information about the selected text.

DevTools window showing the position of the current selection in the console.

It turns out that you can also create a text selection programmatically from JavaScript. Here is an example:

// First, create a Range object.
const range = new Range();

// And set its start and end positions.
range.setStart(parentNode, startOffset);
range.setEnd(parentNode, endOffset);

// Then, set the current selection to this range.
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);

The last piece of the puzzle is to style this range. CSS has a pseudo-element called ::selection to do just that, and it’s supported across all browsers.

::selection {
  background-color: #f06;
  color: white;
}

Here is an example using this technique to highlight all words in a page one after the other:

On top of the ::selection pseudo-element, there are a number of other pseudo-elements:

  • ::target-text selects the text that has been scrolled to in browsers that support the scroll-to-text feature. (MDN)
  • ::spelling-error selects text that is flagged by the browser as containing a spelling error. (MDN)
  • ::grammar-error selects text that is flagged by the browser as containing a grammar error. (MDN)

Unfortunately browser support isn’t great here and although these ranges are useful in each of their own right, they can’t be used to style custom pieces of text — only browser-predefined ones

So the user text selection is nice because it’s relatively simple to put in place and doesn’t change the DOM of the page. Indeed, Range objects are essentially coordinates of segments in the page, rather than HTML elements that need to be created to exist.

One major drawback, however, is that creating a selection resets whatever the user has already manually selected. Try selecting text in the demo above to test this. You’ll see how it goes away as soon as the code moves the selection somewhere else.

Potential Solution #2: Custom Highlighting System

This second solution is pretty much the only thing you can do if using the Selection object is insufficient for you. This solution revolves around doing everything yourself, using JavaScript to insert new HTML elements in the DOM where you want the highlighting to appear.

Unfortunately, this means way more JavaScript to write and maintain, not to mention it forces the browser to re-create the layout of the page whenever the highlighting changes. Plus, there are complicated edge cases, for example, when you want to highlight a piece of text that spans across multiple DOM elements.

Illustration showing a line of HTML with an emphasis element and a strong element with a bright yellow highlight running through them.

Interestingly, CodeMirror and Monaco (the JavaScript text editor library that powers VS Code) have their own highlighting logic. They use a slightly different approach where the highlights are contained in a separate part of the DOM tree. The lines of text and the highlighted segments are rendered in two different places in the DOM which are then positioned over each other. If you inspect the DOM sub-tree that contains the text, there are no highlights. This way, the highlights can be re-rendered without impacting the lines of text and having to introduce new elements within them.

Overall, it feels like a browser-powered highlighting feature is missing. Something that would help solve all of these drawbacks (no interference with user text selection, multi-selection support, simple code) and be faster than custom-made solutions.

Fortunately, that’s what we’re here to talk about!

Enter the CSS Custom Highlight API

The CSS Custom Highlight API is a new W3C specification (currently in Working Draft status) that makes it possible to style arbitrary text ranges from JavaScript! The approach here is very similar to the user text selection technique we reviewed earlier. It gives developers a way to create arbitrary ranges, from JavaScript, and then style them using CSS.

Creating Ranges of Text

The first step is to create the ranges of text that you want to highlight. which can be done using a Range in JavaScript. So, like we did when setting the current selection:

const range = new Range();
range.setStart(parentNode, startOffset);
range.setEnd(parentNode, endOffset);

It’s worth noting that the setStart and setEnd methods work differently if the node passed as the first argument is a text node or not. For text nodes, the offset corresponds to the number of characters within the node. For other nodes, the offset corresponds to the number of child nodes within the parent node.

Also worth noting is that setStart and setEnd aren’t the only ways to describe where a range starts and ends. Take a look at the other methods available on the Range class to see other options.

Creating Highlights

The second step consists in creating Highlight objects for the ranges created in that last step. A Highlight object can receive one or more Ranges. So if you want to highlight a bunch of pieces of text in exactly the same way, you should probably create a single Highlight object and initialize it with all of the Ranges that correspond to these pieces of text.

const highlight = new Highlight(range1, range2, ..., rangeN);

But you can also create as many Highlight objects as you need. For example, if you are building a collaborative text editor where each user gets a different text color, then you can create one Highlight object per user. Each object can then be styled differently, as we’ll see next.

Registering Highlights

Now Highlight objects on their own don’t do anything. They first need to be registered in what is called the highlight registry. This is done by using the CSS Highlights API. The registry works like a map where you can register new highlights by giving them names, as well as remove highlights (or even clear the entire registry).

Here is how to register a single highlight.

CSS.highlights.set('my-custom-highlight', highlight);

Where my-custom-highlight is the name of your choosing and highlight is a Highlight object created in the last step.

Styling Highlights

The final step is to actually style the registered highlights. This is done with the new CSS ::highlight() pseudo-element, using the name you chose when registering the Highlight object (which is my-custom-highlight in our example above).

::highlight(my-custom-highlight) {
  background-color: yellow;
  color: black;
}

It’s worth noting that, just like ::selection, a subset of CSS properties only can be used with the ::highlight() pseudo-element:

Updating Highlights

There are multiple ways to update highlighted text on the page.

For example, you can clear the highlight registry altogether with CSS.highlights.clear() and then start again from the beginning. Or, you can also update the underlying ranges without having to re-create any of the objects all. For this, use the range.setStart and range.setEnd methods again (or any of the other Range methods) and the highlights will be re-painted by the browser.

But, the Highlight object works like a JavaScript Set, so this means you also add new Range objects to an existing Highlight with highlight.add(newRange) or remove a Range with highlight.delete(existingRange).

Third, you can also add or remove specific Highlight objects from the CSS.highlights registry. Since this API works like a JavaScript Map, you can set and delete to update the currently registered Highlights.

Browser Support

The specification for the CSS Custom Highlight API is relatively new and its implementation in browsers is still incomplete. So, although this is going to be a very useful addition to the web platform, it’s not quite ready for production use.

The Microsoft Edge team is implementing the CSS Custom Highlight API in Chromium at the moment. In fact, the feature can already be used in Canary versions right now by enabling the Experimental Web Platform features flag (under about:flags). There is currently no firm plan as to when the feature will ship in Chrome, Edge, and other Chromium-based browsers, but it’s getting very close.

The API is also supported in Safari 99+ but behind an experiment flag (Develop → Experimental Features → Highlight API), and the interface is a little bit different in that it uses StaticRange objects instead.

Firefox does not support the API yet, though you can read Mozilla’s position about it for more information.

Demo

Speaking of Microsoft Edge, they have a demo set up where you can take the CSS Custom Highlight API for a test drive. But Before trying the demo, be sure you’re using either Chrome or Edge Canary with the Experimental Web Platform features flag in the about:flags page.

/button View the demo

The demo uses the Custom Highlight API to highlight ranges of text in the page based on what you type in the search field at the top of the page.

After the page loads, JavaScript code retrieves all the text nodes in the page (using a TreeWalker) and when the user types in the search field, the code iterates over these nodes until it finds matches. Those matches are then used to create Range objects, which are then highlighted with the Custom Highlight API.

Closing Thoughts

So, is this new browser-provided highlighting API really worth it? Totally!

For one, even if the CSS Custom Highlight API may seem a bit complicated at first (i.e. having to create ranges, then highlights, then registering them, and finally styling them), it’s still way simpler than having to create new DOM elements and insert them in the right places.

More importantly, browser engines can style these ranges very, very fast.

The reason only a subset of CSS properties is allowed to be used with the ::highlight() pseudo-element is that the subset only contains properties that can be applied by the browser very effectively without having to recreate the layout of the page. Highlighting ranges of text by inserting new DOM elements in the page around them requires the engine to do much more work.

But don’t take my word for it. Fernando Fiori, who worked on the API, created this nice performance comparison demo. On my computer, the CSS Custom Highlight API performs on average 5✕ as fast as the DOM-based highlighting.

With Chromium and Safari experimental support already here, we’re getting close to something that can be used in production. I can’t wait for browsers to support the Custom Highlight API consistently and see what features this will unlock!


CSS Custom Highlight API: The Future of Highlighting Text Ranges on the Web originally published on CSS-Tricks. You should get the newsletter.