# Listbox

An always-open list let into the plate as a recess. The rows are printed on its floor and a chosen row is a cap let down into it with its lamp lit. One row may be chosen, or several; typing a few letters finds the next row that starts with them. It starts as a native select with a size, so it posts with the form and works without a script.

Kit part · Free · https://tactileui.dev/parts/listbox

- Element: `<tui-listbox>`
- Needs: `kit.css` and `kit.mjs` ([how to install the kit](https://tactileui.dev/docs/installation#the-kit))
- Categories: Lists, Selects
- Finishes: Aluminium, Graphite, Oak, Walnut, Porcelain, Black, Daylight, Night, Paper, Phosphor, E-paper, AMOLED
- Volumes: Tactile, Quiet

## Variants

### Without the script · the select

```html
<tui-listbox data-static style="width:min(260px,100%)"><select class="tui-select" id="lb-v1" name="region" size="4" aria-label="Region"><optgroup label="Europe"><option value="pt" data-hint="eu-south">Portugal</option><option value="ie" data-hint="eu-west" selected>Ireland</option><option value="de" data-hint="eu-central">Germany</option><option value="se" data-hint="eu-north">Sweden</option></optgroup><optgroup label="Americas"><option value="us" data-hint="us-east">United States</option><option value="br" data-hint="sa-east">Brazil</option></optgroup></select></tui-listbox>
```

### One chosen · amber lamp

```html
<tui-listbox data-static style="width:min(260px,100%)"><div class="tui-listbox__list" role="listbox" tabindex="-1" aria-label="Region"><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Portugal</span><span class="tui-listbox__hint">eu-south</span></div><div class="tui-listbox__option" role="option" aria-selected="true"><span class="tui-listbox__text">Ireland</span><span class="tui-listbox__hint">eu-west</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Germany</span><span class="tui-listbox__hint">eu-central</span></div></div></tui-listbox>
```

### Several chosen · green lamps

```html
<tui-listbox data-static style="width:min(260px,100%)"><div class="tui-listbox__list" role="listbox" tabindex="-1" aria-label="Region" aria-multiselectable="true"><div class="tui-listbox__option" role="option" aria-selected="true"><span class="tui-listbox__text">Portugal</span><span class="tui-listbox__hint">eu-south</span></div><div class="tui-listbox__option" role="option" aria-selected="true"><span class="tui-listbox__text">Ireland</span><span class="tui-listbox__hint">eu-west</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Germany</span><span class="tui-listbox__hint">eu-central</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Sweden</span><span class="tui-listbox__hint">eu-north</span></div></div></tui-listbox>
```

### Under the arrow keys

```html
<tui-listbox data-static style="width:min(260px,100%)"><div class="tui-listbox__list" role="listbox" tabindex="-1" aria-label="Region"><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Portugal</span><span class="tui-listbox__hint">eu-south</span></div><div class="tui-listbox__option is-focus" role="option" aria-selected="false"><span class="tui-listbox__text">Ireland</span><span class="tui-listbox__hint">eu-west</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Germany</span><span class="tui-listbox__hint">eu-central</span></div></div></tui-listbox>
```

### In groups, and scrolling

```html
<tui-listbox data-static style="width:min(260px,100%);--rows:5"><div class="tui-listbox__list" role="listbox" tabindex="-1" aria-label="Region"><div role="group" aria-label="Europe"><div class="tui-listbox__heading" aria-hidden="true">Europe</div><div class="tui-listbox__option" role="option" aria-selected="true"><span class="tui-listbox__text">Portugal</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Ireland</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Germany</span></div></div><div role="group" aria-label="Americas"><div class="tui-listbox__heading" aria-hidden="true">Americas</div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">United States</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Brazil</span></div></div></div></tui-listbox>
```

### A row that cannot be chosen

```html
<tui-listbox data-static style="width:min(260px,100%)"><div class="tui-listbox__list" role="listbox" tabindex="-1" aria-label="Region"><div class="tui-listbox__option" role="option" aria-selected="true"><span class="tui-listbox__text">Portugal</span></div><div class="tui-listbox__option" role="option" aria-selected="false" aria-disabled="true"><span class="tui-listbox__text">Ireland</span><span class="tui-listbox__hint">full</span></div><div class="tui-listbox__option" role="option" aria-selected="false"><span class="tui-listbox__text">Germany</span></div></div></tui-listbox>
```

## Every state

- Rest
- Hover
- Active
- Chosen
- Disabled

Hover, pressed and focus are forced with `.is-hover`, `.is-down` and `.is-focus`; checked and disabled are real.

## API

| Name | Kind | Values | What it does |
| - | - | - | - |
|  | element | around one | Hides the select, keeps it as the form’s value and draws the options as rows in the recess. Without the script the select is what shows. |
|  | attribute | on the select | Several rows may be chosen, with green lamps; size is how many rows show before the list scrolls. |
| data-hint · | attribute | on options and groups | A second word printed at the right of a row; a printed heading over a group. |
| value · values · select | property | string · string[] · | What is chosen, and the select itself (listen for change on it). |
| tui-listbox-change | event | detail { values } | Bubbles from the element after the choice changes. The select’s own change fires as ever. |
| ↑ ↓ · Home · End · PageUp · PageDown | key | on the list | Move the active row. In a single list the choice follows it. |
| Space · Shift+↑ ↓ · Ctrl+A | key | in a multiple list | Choose or let go of the active row; choose the run from where you started; choose every row. |
| A letter | key | type-ahead | Typing finds the next row that starts with what you typed; the same letter again steps on to the next that starts with it. |
| typeaheadFind(labels, from, typed) · listboxRange(a, b) · listboxMove(index, key, n) | function | pure | Which row a typed word finds, the rows between two, and where a key moves the active row. |

## Markup

```html
<div class="tui-field">
  <label class="tui-label" for="region">Region</label>
  <tui-listbox>
    <select class="tui-select" id="region" name="region" size="5">
      <optgroup label="Europe">
        <option value="pt" data-hint="eu-south" selected>Portugal</option>
        <option value="ie" data-hint="eu-west">Ireland</option>
      </optgroup>
      …
    </select>
  </tui-listbox>
</div>

<!-- Several: a multiple select; plain presses choose and let go -->
<tui-listbox><select class="tui-select" id="targets" name="targets" size="6" multiple>…</select></tui-listbox>
```

```js
import { typeaheadFind, listboxRange } from '/kit/kit.mjs';

const list = document.querySelector('tui-listbox');
list.addEventListener('tui-listbox-change', (e) => {
  console.log(e.detail.values);               // ['pt']
});

list.values;                                   // the values chosen
typeaheadFind(['Brazil', 'Germany'], -1, 'g'); // 1
listboxRange(1, 3);                            // [1, 2, 3]
```

The demo on the page:

```html
<form class="tui-panel" style="width:min(100%,460px);display:grid;gap:16px;text-align:left" action="/parts/listbox" method="get" data-lb-form><div class="tui-panel__head" style="margin-bottom:0"><span class="tui-label">Deploy · Northwind API</span><span class="tui-status"><span class="tui-lamp" data-on></span>Ready</span></div>
  <div class="tui-field"><label class="tui-label" for="lb-region">Primary region</label><tui-listbox><select class="tui-select" id="lb-region" name="region" size="5"><optgroup label="Europe"><option value="pt" data-hint="eu-south" selected>Portugal</option><option value="ie" data-hint="eu-west">Ireland</option><option value="de" data-hint="eu-central">Germany</option><option value="se" data-hint="eu-north">Sweden</option></optgroup><optgroup label="Americas"><option value="us" data-hint="us-east">United States</option><option value="br" data-hint="sa-east">Brazil</option></optgroup></select></tui-listbox></div>
  <div class="tui-field"><label class="tui-label" for="lb-targets">Also deploy to</label><tui-listbox><select class="tui-select" id="lb-targets" name="targets" multiple size="5"><optgroup label="Europe"><option value="pt" data-hint="eu-south">Portugal</option><option value="ie" data-hint="eu-west" selected>Ireland</option><option value="de" data-hint="eu-central" selected>Germany</option><option value="se" data-hint="eu-north">Sweden</option></optgroup><optgroup label="Americas"><option value="us" data-hint="us-east">United States</option><option value="br" data-hint="sa-east">Brazil</option></optgroup></select></tui-listbox><span class="tui-hint">Space chooses a row, Shift with an arrow a run.</span></div>
  <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap"><output class="tui-muted" style="font-size:var(--tui-t-small)" data-lb-out aria-live="polite">Nothing deployed yet</output><button class="tui-key tui-key--primary" type="submit">Deploy</button></div></form>
```

## Usage notes

Tab to the first list and press the arrow keys: the chosen row follows, as in a native list. Type “br” to jump to Brazil. In the second list Space chooses or lets go of a row, Shift with an arrow chooses a run, and Ctrl+A chooses every row.

## Accessibility

- The WAI-ARIA listbox with `aria-activedescendant`: the list is one tab stop and keeps the focus while the active row moves, and each row has `aria-selected`; a list that takes several is `aria-multiselectable`.
- The label of the select names the list, and the hint and error described on the select are described on the list. A group has its own name, read as the focus enters it.
- A chosen row is a cap let down into the floor with its lamp lit, and it is selected for a screen reader; colour is never the only sign of it. The row under the keys has the 2px focus ring inside its edge.
- Several chosen is said politely: “3 chosen”. In a single list the choice follows the arrows, as in a native select, so a keyboard user hears each row as they pass it.
- On a phone the rows are 44px tall and the press chooses or lets go of a row with no modifier key, so touch needs no Shift or Ctrl.

## How it’s built

The recess is the kit’s well and the rows are the combobox’s lines: a lamp, the words, a hint. A chosen row is drawn as a key let down into the recess, the same pressed shadow a latched key has, so it follows every finish and both volumes. The select stays in the page, hidden, and is written to with a real change event, so a form, a framework or a test reads it as ever. Type-ahead keeps a buffer for 700ms and treats one letter repeated as a request for the next row.

## Rules

Show every option at once instead of hiding it behind a key: arrows, type-ahead, ranges with Shift, and the same select underneath for the form.

The full rules are in `listbox.rules.md` in the brain, which ships with the Kit.

## Related parts

- [Select](https://tactileui.dev/parts/select): A choice from a longer list, in the same recess as a field, with a printed chevron. The browser’s own menu opens it.
- [Combobox](https://tactileui.dev/parts/combobox): A field that filters a long list as you type. It starts as a plain select, so it posts with the form and works without a script; then the options card slides out of a slot under the well, the letters that match print heavier and the chosen option’s lamp is lit. Several choices sit in the well as tags.
- [List](https://tactileui.dev/parts/list): Rows in a rack: a mark, a title, a second line and a fact or a key at the end. Chosen rows sink into a recess with their check key latched; in a running order, a row lifts out of the rack on its skirt and slides past the others, from the keyboard or by its grip.
- [Multi-select](https://tactileui.dev/parts/multi-select): A combobox that takes several. Each choice is set down in the well as a tag, the card of options stays out while you choose the next, and the foot of the card counts what is chosen and lets it all go. It starts as a native select with the multiple attribute, so it posts with the form and works without a script.
