Select

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

Usage

HTML

If you use a <select> element, just add the select class to it or have a parent with the form class (read more about form).

<select class="select w-[180px]">
  <optgroup label="Fruits">
    <option>Apple</option>
    <option>Banana</option>
    <option>Blueberry</option>
    <option>Grapes</option>
    <option>Pineapple</option>
  </optgroup>
</select>

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@0.3.10/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@0.3.10/dist/js/select.min.js" defer></script>

Step 2: Add your select HTML

<div id="select-988050" class="select">
  <button type="button" class="btn-outline w-[180px]" id="select-988050-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-988050-listbox">
    <span class="truncate">Apple</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-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0">
      <path d="m7 15 5 5 5-5" />
      <path d="m7 9 5-5 5 5" />
    </svg>
  </button>
  <div id="select-988050-popover" data-popover aria-hidden="true">
    <header>
      <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-search-icon lucide-search">
        <circle cx="11" cy="11" r="8" />
        <path d="m21 21-4.3-4.3" />
      </svg>
      <input type="text" value="" placeholder="Search entries..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="false" aria-controls="select-988050-listbox" aria-labelledby="select-988050-trigger" />
    </header>

    <div role="listbox" id="select-988050-listbox" aria-orientation="vertical" aria-labelledby="select-988050-trigger">
      <div role="group" aria-labelledby="group-label-select-988050-items-1">
        <div role="heading" id="group-label-select-988050-items-1">Fruits</div>

        <div id="select-988050-items-1-1" role="option" data-value="apple">Apple</div>

        <div id="select-988050-items-1-2" role="option" data-value="banana">Banana</div>

        <div id="select-988050-items-1-3" role="option" data-value="blueberry">Blueberry</div>

        <div id="select-988050-items-1-4" role="option" data-value="grapes">Grapes</div>

        <div id="select-988050-items-1-5" role="option" data-value="pineapple">Pineapple</div>
      </div>
    </div>
  </div>
  <input type="hidden" name="select-988050-value" value="apple" />
</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.

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

Scrollable

Disabled

Invalid State

With icon

Multiple

Enable multiple selection by setting 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.