--- layout: layouts/page.njk title: Select description: Displays a list of options for the user to pick from—triggered by a button. toc: - label: Usage id: usage children: - label: HTML id: usage-html - label: HTML + JavaScript id: usage-html-js children: - label: "Step 1: Include the JavaScript files" id: usage-html-js-1 - label: "Step 2: Add your select HTML" id: usage-html-js-2 - label: HTML structure id: usage-html-js-3 - label: JavaScript events id: usage-html-js-4 - label: JavaScript methods id: usage-html-js-5 - label: Jinja and Nunjucks id: usage-macro - label: Examples id: examples children: - label: Scrollable id: example-scrollable - label: Disabled id: example-disabled - label: With icon id: example-with-icon - label: Multiple id: example-multiple --- {% from "macros/code_preview.njk" import code_preview %} {% from "macros/code_block.njk" import code_block %} {% from "select.njk" import select %} {% set code_native %}{% endset %} {{ code_preview("select-native", code_native) }} {% set code_html %} {{ select( trigger_attrs={"class": "w-[180px]"}, is_combobox=true, multiple=true, items=[ { type: "group", label: "Fruits", items: [ { type: "item", label: "Apple", attrs: {"data-force": "true"} }, { type: "item", label: "Banana", attrs: {"data-keywords": "bread"} }, { type: "item", label: "Blueberry" }, { type: "item", value: "pineapple", label: "Grapes" }, { type: "item", value: "pineapple", label: "Pineapple" } ] } ] ) }} {% endset %} {% set code_script %} {% endset %} {{ code_preview("select", code_script | prettyHtml ~ code_html | prettyHtml) }}

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

{{ code_block(code_native) }}

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:

{% set code_script %} {% endset %} {{ code_block(code_script | prettyHtml, "html") }}
Components with JavaScript {% lucide "arrow-right" %} Use the CLI {% lucide "arrow-right" %} select.js {% lucide "arrow-right" %}

Step 2: Add your select HTML

{{ code_block(code_html | prettyHtml, "html") }}

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

{% set code_value %}{% endset %} {{ code_block(code_value | prettyHtml, "html") }}
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.

{% set code_select %}{% endset %} {{ code_block(code_select | prettyHtml, "html") }}
deselect(value) Multiselect only

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

{% set code_deselect %}{% endset %} {{ code_block(code_deselect | prettyHtml, "html") }}
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").

{% set code_toggle %}{% endset %} {{ code_block(code_toggle | prettyHtml, "html") }}
selectAll() Multiselect only

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

{% set code_script_all %}{% endset %} {{ code_block(code_script_all | prettyHtml, "html") }}
selectNone() Multiselect only

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

{% set code_script_none %}{% endset %} {{ code_block(code_script_none | prettyHtml, "html") }}
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.

Use Nunjucks or Jinja macros {% lucide "arrow-right" %} Jinja macro {% lucide "arrow-right" %} Nunjucks macro {% lucide "arrow-right" %}
{% set raw_code %}{% raw %}{{ 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" } ] } ] ) }}{% endraw %}{% endset %} {{ code_block(raw_code, "jinja") }}

Examples

Scrollable

{% set code %} {% call select( listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"} ) %} {% for i in range(0, 99) %}
0 %}data-value="item-{{ i }}"{% endif %}>Item {{ i }}
{% endfor %} {% endcall %} {% endset %} {{ code_preview("select-scrollable", code | prettyHtml) }}

Disabled

{% set code %} {% call select( trigger_attrs={"disabled": "disabled"} ) %}
Disabled
{% endcall %} {% endset %} {{ code_preview("select-disabled", code | prettyHtml) }}

Invalid State

{% set code %} {{ select( trigger_attrs={"aria-invalid": "true"}, items=[ { value: "", label: "Select a role" }, { value: "admin", label: "Admin" }, { value: "editor", label: "Editor" }, { value: "viewer", label: "Viewer" }, { value: "guest", label: "Guest" } ] ) }} {% endset %} {{ code_preview("select-disabled", code | prettyHtml) }}

With icon

{% set code %} {% call select( id="select-with-icons", trigger_attrs={'class': 'w-[180px]'} ) %}
{% lucide "chart-bar", {"class": "text-muted-foreground"} %} Bar
{% lucide "chart-line", {"class": "text-muted-foreground"} %} Line
{% lucide "chart-pie", {"class": "text-muted-foreground"} %} Pie
{% endcall %} {% endset %} {{ code_preview("select-with-icons", code | prettyHtml) }}

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.

{% set code %} {% call select( id="select-multiple", name="chart-types", multiple=true, placeholder="Select chart types...", selected=["bar", "line"], trigger_attrs={"class": "w-[220px]"} ) %}
{% lucide "chart-bar", {"class": "text-muted-foreground"} %} Bar
{% lucide "chart-line", {"class": "text-muted-foreground"} %} Line
{% lucide "chart-pie", {"class": "text-muted-foreground"} %} Pie
{% lucide "chart-area", {"class": "text-muted-foreground"} %} Area
{% endcall %} {% endset %} {{ code_preview("select-multiple", code | prettyHtml) }}