Select

Sizes: sm, default and lg

A native select only needs the select class. No script. It is as tall as a .btn of the same size.

Select with a styled list

Wrap a real <select> in <div class="select">. The script draws the button and the list, and selects the matching option in the original select, so the form submits as usual. Without the script you get the native select. A hidden value="" option is the text shown before anything is chosen.

Sizes of a select with a styled list

data-size on the wrapper takes the same values as the native select.

Styled list with group headings

Use <optgroup label> as usual. Each group becomes a role="group" with a heading.

Options with a title and a line of text

data-text on an option adds a line of text. data-badge adds a badge after the title. Search also matches the text. The button shows only the title.

Options with an avatar, a title and text; a disabled option

data-avatar takes initials or an image URL. A disabled option is dimmed and cannot be chosen (aria-disabled="true" in the list).

Select with a search field in its list

data-search adds a search field above the list. Its value is the placeholder. Set the no-results text with data-empty.

Select with more than one choice: type to filter the list

Add multiple to the select. The control shows a chip for each choice and a text field that filters the list. Backspace in the empty field removes the last chip.

One choice typed in the control, with an action above the list

data-input turns the control into a text field for one choice. A <header> in the wrapper moves to the top of the list (without the script it sits above the select).

Remote options: data-url loads the options from the server

data-url on the wrapper is the search URL. Results are added as <option>s in the real select, so the form submits as usual. If there is no text field, the script adds a search field to the list.

Each request sends q (the typed text, left out when empty) and page (starting at 1). A {page} in the URL is replaced with the page number too, so static JSON files work, e.g. data/countries-{page}.json.

GET data/countries-{page}.json?q=fra&page=1

{
  "results": [{ "id": "fr", "text": "France" }],
  "pagination": { "more": false }
}

The page filters the results again by text (the typed words must be in text), so a server that ignores q, like a static file, still gives the right list. A real server should return only items whose text contains the query.

Remote options with several choices, starting with France

The starting choice is written in the page as <option selected>. No request is sent until the list opens.

Remote options with one choice: scroll to the end to load the next page

A single select needs an <option value=""> as the empty value. When the reply has "more": true and the list is scrolled near the end (50px), the next page is requested.

What a select with remote options does, case by case

When What happens
The page loads No request. Options in the markup are the starting choice.
The list opens One request for page 1, without q. The list shows Searching… until the reply arrives.
A key is typed Aborts the previous request (AbortController) and sends a new one with q and page=1. While waiting, the current list is filtered in the page right away.
An older reply arrives late Ignored, because that request was aborted.
The reply has "more": true The listbox gets data-more and a Loading more… line below the list.
Scrolled within 50px of the end Requests the next page with the same q and appends it. No repeat request while waiting (aria-busy). If the list is too short to scroll, the next page is requested automatically.
No results Shows No results, or the text in data-empty.
The request fails or is not JSON Clears the list. The listbox gets data-error and shows Could not load results, and the error is logged to the console. The next keystroke retries.
An option is chosen Same as any select: the option in the real select is selected, and input/change and tsunami:select:change fire. A multiple select clears the text field and requests page 1 again.
A chosen option is not in the reply It stays in the real select as hidden, so it is still selected and submitted with the form, but not shown in the list.
A chosen option is in the reply Shown once, in the server's order, with a check mark.
The list closes Aborts any pending request and clears the search field. Opening again sends a new request. No cache.

Tags: data-tags lets the user create an option by typing it

data-tags offers the typed text as a new option (its value is that text). Type a word that is not in the list and press Enter. The new option becomes an <option data-tag> in the real select.

Tags with remote options: choose a country from the server or create a new name

What a select with tags does, case by case

When What happens
The text matches no option The text is offered as the first option (plus icon) and is active, so Enter creates it right away.
The text matches an option, in any case No tag is offered. "SALE" finds "sale".
Only spaces No tag is offered. Leading and trailing spaces are trimmed.
The offered tag is chosen Creates an <option data-tag selected> in the real select. Its value and label are the typed text.
The text changes first The old offered tag goes away and the new text is offered instead.
A created tag is removed It stays in the list until the next keystroke, then is removed from the select.
With data-url too The tag shows above the server results, unless a result has the same name (once the reply arrives).

API

Every function takes any element inside the wrapper (or the wrapper itself) as its first argument. A native select with only the select class has no API.

Function Arguments Description
tsunami.select.open(el) el: Element Opens the list (does nothing if the select is disabled).
tsunami.select.close(el, focus) el: Element, focus: boolean (default false) Closes the list. If focus is true, focus moves back to the control.
tsunami.select.value(el) el: Element Returns a string for a single select, or an array of strings for multiple.
tsunami.select.value(el, v) v: string or string[] Selects the existing options with these values. Fires events when the value changes.
tsunami.select.refresh(el) el: Element Redraws the control after code adds, removes or edits options (dispatching change on the select also redraws).
Event On Detail
tsunami:select:change wrapper, bubbles { value, selected: [{ value, label }] } Fired when the value changes by click, keyboard or value(). Not fired when the same value is chosen again.
input, change real select, bubbles Fired together, so forms and the Save bar react as if the user chose in the select itself.
tsunami:select:open wrapper, bubbles No detail. Fired when the list opens.
tsunami:select:close wrapper, bubbles No detail. Fired when the list closes.
Key Description
↓ ↑ Closed: opens the list. In the list: moves the active option.
Home End Go to the first/last option (on the button). In a text field they move the cursor as usual.
Enter, Space On a closed button: opens. In the list: chooses the active option (Space works on the button only).
Letters On the button: opens the list and jumps to the option starting with the typed letters (type-ahead). With a search field, the letters go into the search field.
Backspace Multiple, empty text field: removes the last chip
Escape Closes the list and returns focus to the control
Tab Leaves the field and closes the list

Attributes

Attribute / element Values Description
select.select Styled native select. No script.
div.select Wrapper of a direct child <select>. The script draws the control and the list. Without the script, the original select shows.
data-size sm, lg (none = default 2rem) Height. Works on both select.select and div.select.
data-placeholder text (default: the text of option[value=""]) Text shown before anything is chosen. Also the text field's placeholder.
data-search present / absent; the value is the placeholder (default "Search") Adds a search field above the list (button control)
data-input present / absent (default absent) Makes the control a text field for one choice. multiple always uses a text field.
data-tags present / absent (default absent) Offers the typed text as a new option
data-url URL, supports {page} URL to load JSON options with ?q=&page=. Needs a text field (added automatically).
data-empty text (default "No results") Text shown when the search finds nothing
<header> in the wrapper Moved to the top of the list, for action buttons such as Create new customer
<select multiple> Several choices. The control shows chips and a text field. The listbox has aria-multiselectable.
<optgroup label> Group with a heading (role="group" + <strong>)
option[value=""] Add hidden to keep it out of the list Empty value = nothing chosen. Required for a single select with data-url.
option[selected] Starting choice
option[disabled] Cannot be chosen. Shown dimmed with aria-disabled="true".
option[hidden] Not shown in the list, but can still be selected and submitted
option[data-label] text (default: the option's text) Label shown on the button/chip and used for search
option[data-text] text Line of text under the title. Searchable.
option[data-avatar] initials or image URL Small avatar before the option
option[data-badge] text Badge after the title
option[data-tag] set by the script Option created from typed text (data-tags)
data-ready set by the script On the wrapper once the control is drawn. The real select is visually hidden (it still submits and validates).
data-has-value set by the script On the wrapper while a value is chosen
role="combobox" set by the script The button or text field, with aria-expanded, aria-controls and aria-activedescendant. Its label comes from the select's <label for> or aria-label.
[popover] > role="listbox" set by the script A popover="manual" list placed below the control (flips above if there is no room). Each role="option" has aria-selected and data-active.
aria-busy, data-more, data-error set by the script On the listbox with data-url: loading, more pages, load failed