Select

Displays a list of options for the user to pick from—triggered by a button.

Usage

HTML + JavaScript

Step 1: Include the JavaScript files

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0/dist/js/select.min.js" defer></script>

Step 2: Add your select HTML

<div id="select-468201" class="select">
  <button type="button" class="w-[180px]" id="select-468201-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-468201-listbox">
    <span class="truncate">Light</span>
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
  </button>
  <div id="select-468201-popover" data-popover aria-hidden="true">
    <div role="listbox" id="select-468201-listbox" aria-orientation="vertical" aria-labelledby="select-468201-trigger">
      <div role="group" aria-labelledby="group-label-select-468201-items-1">
        <div role="heading" id="group-label-select-468201-items-1">Theme</div>

        <div id="select-468201-items-1-1" role="option" data-value="light">Light</div>

        <div id="select-468201-items-1-2" role="option" data-value="dark">Dark</div>

        <div id="select-468201-items-1-3" role="option" data-value="system">System</div>
      </div>
    </div>
  </div>
  <input type="hidden" name="select-468201-value" value="light" />
</div>

HTML structure

<div class="select">

Wraps around the entire component. Can have the following attributes:

  • data-placeholder="{ TEXT }" Optional: placeholder text shown when no options are selected (multiselect only).
  • data-close-on-select="true" Optional: closes the popover when selecting an option in multiselect mode.
<button type="button">

The trigger to open the popover. Should have the following attributes:

  • id="{BUTTON_ID}": linked to by the aria-labelledby attribute of the listbox.
  • aria-haspopup="listbox": indicates that the button opens a listbox.
  • aria-controls="{ LISTBOX_ID }": points to the listbox's id.
  • aria-expanded="false": tracks the popover's state.
  • aria-activedescendant="{ OPTION_ID }" Optional: points to the active option's id.
<div data-popover aria-hidden="true" id="{ POPOVER_ID }">
The popover content. You can set up the side and alignment using the data-side and data-align attributes (see Popover component).
<div role="listbox">

The listbox containing the options. Should have the following attributes:

  • id="{ LISTBOX_ID }": refered to by the aria-controls attribute of the trigger.
  • aria-labelledby="{ BUTTON_ID }": linked to by the button's id attribute.
  • aria-multiselectable="true" Optional: enables multiple selection mode.
<div role="option" data-value="{ VALUE }">

Option that can be selected. Should have the following attributes:

  • id="{ OPTION_ID }" Optional: unique id for this option (needed if you use aria-activedescendant on the trigger).
  • data-value="{ VALUE }": the value for this option.
  • data-label="{ LABEL }" Optional: the text label to use for this option when selected in multiple mode. If not provided, the option's text content will be used (HTML stripped).
  • aria-selected="true" Optional: indicates this option is selected.
<hr role="separator"> Optional
Separator between groups/options.
<div role="group"> Optional
Group of options, can have a aria-labelledby attribute to link to a heading.
<span role="heading"> Optional
Group heading, must have an id attribute if you use the aria-labelledby attribute on the group.
<input type="hidden" name="{ NAME }" value="{ VALUE }">

The hidden input that holds the selected value.

For single-select: contains the selected option's value as a string.

For multiselect: contains a JSON array of selected values (e.g., ["apple","banana"]). When no options are selected, contains an empty array ([]).

Backend handling: Parse the JSON value on the server side. For example:

  • Python/Flask: values = json.loads(request.form.get('field', '[]'))
  • PHP: $values = json_decode($_POST['field'] ?? '[]', true);
  • Node.js: const values = JSON.parse(req.body.field || '[]');
  • Ruby/Rails: values = JSON.parse(params[:field] || '[]')

JavaScript events

basecoat:initialized
Once the component is fully initialized, it dispatches a custom (non-bubbling) basecoat:initialized event on itself.
basecoat:popover
When the popover opens, the component dispatches a custom (non-bubbling) basecoat:popover event on document. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.
change

When the selected value changes, the component dispatches a custom (bubbling) change event on itself, with the selected value in event.detail.value:

  • Single select: { detail: { value: "something" }} (string)
  • Multiple select: { detail: { value: ["item1", "item2"] }} (array)

JavaScript methods and properties

value (property)

Get or set the current value. For single-select, this is a string. For multiselect, this is an array. The multiselect setter accepts both strings and arrays.

<script>
  const selectComponent = document.querySelector("#my-select");
  selectComponent.addEventListener("basecoat:initialized", () => {
    // Get value
    console.log(selectComponent.value); // 'apple'

    // Set value (single-select)
    selectComponent.value = "banana";

    // Set value (multiselect - accepts string or array)
    selectComponent.value = ["apple", "banana"];
    selectComponent.value = "apple"; // normalized to ['apple']
  });
</script>
select(value)

Selects an option by value (i.e. the option with the matching data-value attribute). For single-select, this will close the popover. For multiselect, this adds the value to the selection if not already selected.

<script>
  const selectComponent = document.querySelector("#my-select");
  selectComponent.addEventListener("basecoat:initialized", () => {
    selectComponent.select("apple");
  });
</script>
deselect(value) Multiselect only

Removes a specific value from the selection. Only available when the component is in multiselect mode (aria-multiselectable="true").

<script>
  const selectComponent = document.querySelector("#my-multiselect");
  selectComponent.addEventListener("basecoat:initialized", () => {
    selectComponent.deselect("apple");
  });
</script>
toggle(value) Multiselect only

Toggles a specific value in the selection (adds if not present, removes if present). Only available when the component is in multiselect mode (aria-multiselectable="true").

<script>
  const selectComponent = document.querySelector("#my-multiselect");
  selectComponent.addEventListener("basecoat:initialized", () => {
    selectComponent.toggle("apple"); // adds if not selected, removes if selected
  });
</script>
selectAll() Multiselect only

Selects all available options. Only available when the component is in multiselect mode (aria-multiselectable="true").

<script>
  const selectComponent = document.querySelector("#my-multiselect");
  selectComponent.addEventListener("basecoat:initialized", () => {
    selectComponent.selectAll();
  });
</script>
selectNone() Multiselect only

Deselects all options. Only available when the component is in multiselect mode (aria-multiselectable="true").

<script>
  const selectComponent = document.querySelector("#my-multiselect");
  selectComponent.addEventListener("basecoat:initialized", () => {
    selectComponent.selectNone();
  });
</script>
selectByValue(value)

Deprecated: Alias for select(value). Kept for backward compatibility.

refresh()
Rescans options after changing children inside the existing role="listbox" element.
window.basecoat.refresh(selectComponent)
Calls the component refresh method through the global dispatcher.

Jinja and Nunjucks

You can use the select() Nunjucks or Jinja macro for this component.

{{ select(
  items=[
    {
      type: "group",
      label: "Fruits",
      items: [
        { type: "item", value: "apple", label: "Apple" },
        { type: "item", value: "banana", label: "Banana" },
        { type: "item", value: "blueberry", label: "Blueberry" },
        { type: "item", value: "grapes", label: "Grapes" },
        { type: "item", value: "pineapple", label: "Pineapple" }
      ]
    }
  ]
) }}

Examples

Align

Use data-align on the popover to align the list to the trigger edge. Basecoat keeps the popup below the control; it does not implement shadcn/ui's selected-item-over-trigger positioning.

Groups

Scrollable

Disabled

Invalid

With icon

Multiple

Enable multiple selection by adding aria-multiselectable="true" to the listbox. The selected options are displayed as a comma-separated list in the trigger button. Use data-label to specify clean text labels for options that contain HTML (like icons), otherwise the text content will be extracted automatically.

RTL

Set dir="rtl" on the select root or a parent element.