---
language: "en"
title: "Select"
canonical: "https://uniaria.tenphi.me/components/select/"
source: "docs/components/select.mdx"
index: true
---

# Select

A select lets the user pick one option, or several in multiple mode, from a
list that opens under a trigger button. Use it instead of a native `<select>`
when the options need markup, groups with headings, descriptions or a styled
popup. Select is a [Listbox](/components/listbox/) inside a [Popover](/components/popover/),
plus the trigger: the options, keys, typeahead and selection are Listbox's,
and the popover holds the list in the top layer, places it and closes it. A
hidden native `<select>` carries the value into forms and takes the browser's
autofill.

The trigger follows the WAI-ARIA
[select-only combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/)
and the popup the [Listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/),
except that focus moves into the list while it is open (see
[Accessibility](#accessibility)).

## Usage

```tsx
import { select } from '@uniaria/core/select';
import { useBehavior } from '@uniaria/react';

const fruits = [
  { key: 'apple', textValue: 'Apple' },
  { key: 'banana', textValue: 'Banana' },
  { key: 'cherry', textValue: 'Cherry' },
];

export function FruitSelect() {
  const fruit = useBehavior(select, {
    items: fruits,
    placeholder: 'Pick a fruit',
    isRequired: true,
  });
  return (
    <div {...fruit.parts.root}>
      <span {...fruit.parts.label}>Fruit</span>
      <button {...fruit.parts.trigger}>
        <span {...fruit.parts.value} />
        <ChevronIcon aria-hidden />
      </button>
      <div {...fruit.parts.popover}>
        <div {...fruit.parts.list}>
          {fruits.map((item) => (
            <div key={item.key} {...fruit.getItemProps('option', item.key)}>
              {item.textValue}
            </div>
          ))}
        </div>
        <p {...fruit.parts.empty}>No fruit</p>
      </div>
      <select {...fruit.parts.hiddenSelect} name="fruit" />
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { select } from '@uniaria/core/select';
import { useBehavior } from '@uniaria/vue';

const fruits = [
  { key: 'apple', textValue: 'Apple' },
  { key: 'banana', textValue: 'Banana' },
  { key: 'cherry', textValue: 'Cherry' },
];

const fruit = useBehavior(select, {
  items: fruits,
  placeholder: 'Pick a fruit',
  isRequired: true,
});
</script>

<template>
  <div v-bind="fruit.parts.root">
    <span v-bind="fruit.parts.label">Fruit</span>
    <button v-bind="fruit.parts.trigger">
      <span v-bind="fruit.parts.value" />
      <ChevronIcon aria-hidden="true" />
    </button>
    <div v-bind="fruit.parts.popover">
      <div v-bind="fruit.parts.list">
        <div
          v-for="item in fruits"
          :key="item.key"
          v-bind="fruit.getItemProps('option', item.key)"
        >
          {{ item.textValue }}
        </div>
      </div>
      <p v-bind="fruit.parts.empty">No fruit</p>
    </div>
    <select v-bind="fruit.parts.hiddenSelect" name="fruit" />
  </div>
</template>
```

```html
<uni-select placeholder="Pick a fruit" required>
  <div data-uni="select">
    <span data-element="Label">Fruit</span>
    <button data-element="Trigger">
      <span data-element="Value"></span>
      <svg aria-hidden="true">…</svg>
    </button>
    <div data-element="Popover">
      <div data-element="List">
        <div data-element="Option" data-key="apple">Apple</div>
        <div data-element="Option" data-key="banana">Banana</div>
        <div data-element="Option" data-key="cherry">Cherry</div>
      </div>
      <p data-element="Empty">No fruit</p>
    </div>
    <select data-element="HiddenSelect" name="fruit"></select>
  </div>
</uni-select>

<script type="module">
  import '@uniaria/elements/select';
</script>
```

The options are Listbox's item parts, so keys, groups, option labels,
descriptions and extras, and items read from the markup work as on
[Listbox](/components/listbox/#items-and-keys). Render the options while the list is
closed, inside the hidden popover: the element reads its items from them, and
the trigger's `aria-controls` points at a list that holds the selected option.

Render the `root` part around everything, the popover included. Select tracks
focus within it: without a root, `focuschange` never fires and the
`data-focus-within` and `data-focus-visible` attributes never appear.

### The displayed value

UniAria writes the `value` part's text: the selected option's `getTextValue`,
the selected options' texts joined with `, ` in multiple mode, or the
`placeholder` while nothing is selected, when it also sets
`data-placeholder`. Leave the part empty. In React and Vue the text appears once
the component mounts.

For an icon or other markup in the trigger, leave the `value` part out and
render the value yourself:

```tsx
const selected = useBehaviorState(fruit, (state) => state.value[0]);
return (
  <button {...fruit.parts.trigger}>
    {selected ? <FruitBadge fruit={selected} /> : 'Pick a fruit'}
  </button>
);
```

```vue
<script setup lang="ts">
const fruit = useBehavior(select, { items: fruits });
const selected = useBehaviorState(fruit, (state) => state.value[0]);
</script>

<template>
  <button v-bind="fruit.parts.trigger">
    <FruitBadge v-if="selected" :fruit="selected" />
    <template v-else>Pick a fruit</template>
  </button>
</template>
```

```js
const fruit = document.querySelector('uni-select');
const badge = fruit.querySelector('.badge');
fruit.addEventListener('valuechange', () => {
  const { valueText, isPlaceholder } = fruit.instance.getState();
  badge.textContent = isPlaceholder ? 'Pick a fruit' : valueText;
});
```

### Trigger extras

For buttons, badges and counts that go with the trigger, such as a quiet
Button that clears the value, render a `triggerFrame` inside the root, around
the trigger, and a `triggerExtras` part in it, beside the trigger. The extras
are never inside the trigger, so each is a control of its own and a Tab stop
after the trigger. The frame and the extras carry the trigger's state
attributes, `data-open` and `data-invalid` included, and the pointer anywhere
over the frame hovers the trigger.

```tsx
import { useState } from 'react';
import { button } from '@uniaria/core/button';
import { select } from '@uniaria/core/select';
import { useBehavior } from '@uniaria/react';

export function FruitSelect() {
  const [value, setValue] = useState<readonly string[]>([]);
  const fruit = useBehavior(select, {
    items: fruits,
    placeholder: 'Pick a fruit',
    value,
    onValueChange: setValue,
  });
  const clear = useBehavior(button, { onPress: () => setValue([]) });
  return (
    <div {...fruit.parts.root}>
      <span {...fruit.parts.label}>Fruit</span>
      <div {...fruit.parts.triggerFrame}>
        <button {...fruit.parts.trigger}>
          <span {...fruit.parts.value} />
        </button>
        <span {...fruit.parts.triggerExtras}>
          <button
            {...clear.parts.root}
            data-variant="quiet"
            aria-label="Clear Fruit"
          >
            <ClearIcon aria-hidden />
          </button>
        </span>
      </div>
      <div {...fruit.parts.popover}>…</div>
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { button } from '@uniaria/core/button';
import { select } from '@uniaria/core/select';
import { useBehavior } from '@uniaria/vue';

const value = ref<readonly string[]>([]);
const fruit = useBehavior(select, () => ({
  items: fruits,
  placeholder: 'Pick a fruit',
  value: value.value,
  onValueChange: (keys) => (value.value = keys),
}));
const clear = useBehavior(button, { onPress: () => (value.value = []) });
</script>

<template>
  <div v-bind="fruit.parts.root">
    <span v-bind="fruit.parts.label">Fruit</span>
    <div v-bind="fruit.parts.triggerFrame">
      <button v-bind="fruit.parts.trigger">
        <span v-bind="fruit.parts.value" />
      </button>
      <span v-bind="fruit.parts.triggerExtras">
        <button
          v-bind="clear.parts.root"
          data-variant="quiet"
          aria-label="Clear Fruit"
        >
          <ClearIcon aria-hidden="true" />
        </button>
      </span>
    </div>
    <div v-bind="fruit.parts.popover">…</div>
  </div>
</template>
```

```html
<uni-select id="fruit" placeholder="Pick a fruit">
  <div data-uni="select">
    <span data-element="Label">Fruit</span>
    <div data-element="TriggerFrame">
      <button data-element="Trigger"><span data-element="Value"></span></button>
      <span data-element="TriggerExtras">
        <uni-button id="clear-fruit">
          <button
            data-uni="button"
            data-variant="quiet"
            aria-label="Clear Fruit"
          >
            <svg aria-hidden="true">…</svg>
          </button>
        </uni-button>
      </span>
    </div>
    <div data-element="Popover">…</div>
  </div>
</uni-select>

<script type="module">
  import '@uniaria/elements/button';
  import '@uniaria/elements/select';

  const fruit = document.querySelector('#fruit');
  fruit.addEventListener('valuechange', (event) => {
    fruit.value = event.detail;
  });
  document.querySelector('#clear-fruit').addEventListener('press', () => {
    fruit.value = [];
  });
</script>
```

Without an anchor part, the popover is placed against the frame, so
`min-width: var(--anchor-width)` makes it as wide as the trigger with its
extras. A press on an extra while the list is open closes the list without
selecting, and the extra acts, in one press; focus stays on the extra. Name
each extra in its context: "Clear Fruit", not "Clear".

### Tooltips

Select takes two [Tooltip](/components/tooltip/) links:

* **`tooltip`** is the trigger's, as on a Button. It describes the trigger
  after its description and error, unless it has `relation: 'none'`.
* **`optionTooltip`** is one tooltip for every option. It opens on the option
  under the pointer or with keyboard focus, moves with them, and closes on a
  press or while you type. It never describes an option: give it
  `relation: 'none'`, or development builds warn. Render it inside the
  popover, beside the list, never inside an option, whose name comes from its
  content.

With `shouldOpenOnlyWhenTruncated`, a tooltip opens only while the text is cut
off, and writes the whole text into its `text` part (see
[Overflow tooltips](/components/tooltip/#overflow-tooltips)). The trigger's measures
the `value` part, so a cut-off value gets an overflow tooltip; the option
tooltip measures each option's `optionLabel` and `optionDescription`, else
the option.

```tsx
import { select } from '@uniaria/core/select';
import { tooltip } from '@uniaria/core/tooltip';
import { useBehavior } from '@uniaria/react';

export function FruitSelect() {
  const valueTip = useBehavior(tooltip, {
    shouldOpenOnlyWhenTruncated: true,
    relation: 'none',
  });
  const optionTip = useBehavior(tooltip, {
    shouldOpenOnlyWhenTruncated: true,
    relation: 'none',
    positionArea: 'right',
  });
  const fruit = useBehavior(select, {
    items: fruits,
    tooltip: valueTip.instance,
    optionTooltip: optionTip.instance,
  });
  return (
    <div {...fruit.parts.root}>
      <span {...fruit.parts.label}>Fruit</span>
      <button {...fruit.parts.trigger}>
        <span {...fruit.parts.value} />
      </button>
      <div {...valueTip.parts.root}>
        <span {...valueTip.parts.text} />
      </div>
      <div {...fruit.parts.popover}>
        <div {...fruit.parts.list}>…</div>
        <div {...optionTip.parts.root}>
          <span {...optionTip.parts.text} />
        </div>
      </div>
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { select } from '@uniaria/core/select';
import { tooltip } from '@uniaria/core/tooltip';
import { useBehavior } from '@uniaria/vue';

const valueTip = useBehavior(tooltip, {
  shouldOpenOnlyWhenTruncated: true,
  relation: 'none',
});
const optionTip = useBehavior(tooltip, {
  shouldOpenOnlyWhenTruncated: true,
  relation: 'none',
  positionArea: 'right',
});
const fruit = useBehavior(select, {
  items: fruits,
  tooltip: valueTip.instance,
  optionTooltip: optionTip.instance,
});
</script>

<template>
  <div v-bind="fruit.parts.root">
    <span v-bind="fruit.parts.label">Fruit</span>
    <button v-bind="fruit.parts.trigger">
      <span v-bind="fruit.parts.value" />
    </button>
    <div v-bind="valueTip.parts.root">
      <span v-bind="valueTip.parts.text" />
    </div>
    <div v-bind="fruit.parts.popover">
      <div v-bind="fruit.parts.list">…</div>
      <div v-bind="optionTip.parts.root">
        <span v-bind="optionTip.parts.text" />
      </div>
    </div>
  </div>
</template>
```

```html
<uni-tooltip open-only-when-truncated relation="none">
  <uni-select option-tooltip="fruit-option-tip">
    <div data-uni="select">
      <span data-element="Label">Fruit</span>
      <button data-element="Trigger"><span data-element="Value"></span></button>
      <div data-element="Popover">
        <div data-element="List">…</div>
        <uni-tooltip
          id="fruit-option-tip"
          open-only-when-truncated
          relation="none"
          position-area="right"
        >
          <div data-uni="tooltip"><span data-element="Text"></span></div>
        </uni-tooltip>
      </div>
    </div>
  </uni-select>
  <div data-uni="tooltip"><span data-element="Text"></span></div>
</uni-tooltip>

<script type="module">
  import '@uniaria/elements/select';
  import '@uniaria/elements/tooltip';
</script>
```

The `<uni-tooltip>` around the select is the trigger's tooltip. The option
tooltip is linked only by its id, with `option-tooltip`, never through a
`<uni-tooltip>` around the select. Import the select's entry before the
tooltip's.

### Forms

Put an empty `<select>` in the `hiddenSelect` part, with your `name`, and
`form` or `autocomplete` when you need them. UniAria fills it with an empty
first `<option>` and one `<option>` per item, keeps its selection, `multiple`,
`required` and `disabled` in step, and hides it visually, but not with
`display: none`, since Safari won't autofill that. Then:

* The form submits the selected key under the `<select>`'s name, once per key
  in multiple mode.
* With `isRequired`, the browser blocks submitting nothing. When the select
  is the form's first invalid control, focus moves to its trigger and shows.
  `form.checkValidity()` moves no focus.
* Resetting the form goes back to `defaultValue`, even one set after the
  select rendered, and `valueChange` reports it. A controlled select gets a
  `valueChange` asking for `defaultValue`, or without one the value it
  mounted with.
* Browser autofill sets the value.

### Multiple selection

With `selectionMode: 'multiple'` (`selection-mode="multiple"`), a press, Enter
or Space toggles an option and the list stays open. Listbox's ranges and Ctrl+A
(Cmd+A on Apple platforms) work in the list, and Escape, Tab or a press outside
closes it. The value text joins the selected options with `, `. Typing and
ArrowLeft and ArrowRight on the closed trigger change nothing in multiple mode.

### Controlled value and open state

```tsx
const [value, setValue] = useState<readonly string[]>([]);
const [isOpen, setOpen] = useState(false);
const fruit = useBehavior(select, {
  items: fruits,
  value,
  onValueChange: setValue,
  isOpen,
  onOpenChange: setOpen,
});
```

```vue
<script setup lang="ts">
const value = ref<readonly string[]>([]);
const isOpen = ref(false);
const fruit = useBehavior(select, () => ({
  items: fruits,
  value: value.value,
  onValueChange: (keys) => (value.value = keys),
  isOpen: isOpen.value,
  onOpenChange: (open) => (isOpen.value = open),
}));
</script>
```

```js
const fruit = document.querySelector('uni-select');
fruit.value = ['banana'];
fruit.addEventListener('valuechange', (event) => {
  fruit.value = event.detail;
});
```

Setting the `value` or `isOpen` property makes it controlled: the element
reports the change and waits for you to set the new value. Set `defaultValue`
or `defaultOpen` to let it keep its own. Reading the property gives the
current value either way.

A controlled select that keeps `isOpen` true stays open after Escape, Tab or
a choice.

### Virtualization

Select takes Listbox's virtualization props and passes them to its list. Put
the `scroller` part inside the popover, around the list, and render the
`renderedKeys` state as on [Listbox](/components/listbox/#virtualization). Rows are
positioned absolutely, so give the popover a width, such as
`width: var(--anchor-width)`. Opening scrolls the selected option into view.

## Anatomy

| Part            | Element                    | Notes                                                                                                                                                                                                          |
| --------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `root`          | any, usually `div`         | Around every other part. Select tracks focus within it.                                                                                                                                                        |
| `label`         | any, `span` by default     | Names the trigger and the list. A press on it focuses the trigger without opening the list. UniAria writes no `for`, even on a `<label>`.                                                                      |
| `triggerFrame`  | any, usually `div`         | Optional, inside the root, around the trigger and its extras. It carries the trigger's state attributes, and the pointer over it hovers the trigger. Without an anchor part, the popover is placed against it. |
| `trigger`       | `button`                   | Opens and closes the list. Gets `role="combobox"`; its text is the value. Add an icon hidden with `aria-hidden`.                                                                                               |
| `value`         | any, usually `span`        | Optional, inside the trigger, left empty: UniAria writes the value text or the placeholder.                                                                                                                    |
| `triggerExtras` | any, usually `span`        | Optional, inside the frame, beside the trigger and never inside it: your buttons, badges and counts. It carries the trigger's state attributes.                                                                |
| `anchor`        | any                        | Optional. The popover is placed against it instead of the frame or the trigger.                                                                                                                                |
| `popover`       | any, usually `div`         | Always rendered, after the trigger. Hidden while closed; shown in the top layer while open.                                                                                                                    |
| `arrow`         | any, inside the popover    | Optional. Points at the trigger, or the anchor.                                                                                                                                                                |
| `scroller`      | any, usually `div`         | Only when virtualized: the box that scrolls, around the list.                                                                                                                                                  |
| `list`          | any, usually `div` or `ul` | Inside the popover. Holds the options and groups.                                                                                                                                                              |
| `empty`         | any                        | Optional, inside the popover and outside the list: your "No items" text, hidden while there are options.                                                                                                       |
| `close`         | `button`                   | Optional, inside the popover. A visually hidden one lets touch screen-reader users close the list.                                                                                                             |
| `description`   | any                        | Optional. Describes the trigger.                                                                                                                                                                               |
| `error`         | any                        | Optional. Shown and announced while `isInvalid`.                                                                                                                                                               |
| `errorText`     | any, inside `error`        | Optional. In a [Form](/components/form/), UniAria writes the first error's text here; leave it out to render errors yourself.                                                                                  |
| `hiddenSelect`  | `select`, left empty       | Optional. Submits the value and takes autofill; UniAria writes its options.                                                                                                                                    |

A part keeps an `id` you give it, and everything that refers to the part
follows it; UniAria writes an `id` only on a part that has none (see
[Ids](/guide/accessibility/#ids)).

The item parts are Listbox's: `option`, `optionLabel`, `optionDescription`,
`optionExtras`, `group` and `groupLabel`, inside the list and keyed by the
item's key (see [Listbox](/components/listbox/#anatomy)). `optionExtras` holds
badges, counts or icons inside an option, nothing focusable, and describes the
option after its description; extras with `aria-hidden="true"` stay out of
the description (see [Extras in options](/components/listbox/#extras-in-options)).

## Props

| Prop                                                                            | Attribute                                   | Type                     | Default                       | Description                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `items`, `getKey`, `getTextValue`, `isItemDisabled`, `getChildren`              | — (properties, or markup)                   |                          |                               | Listbox's: the options and how to read them. `getTextValue` is also the displayed value and the hidden `<option>`s' text.                                                                                                |
| `disabledKeys`                                                                  | `disabled-keys` (JSON)                      | `readonly string[]`      | `[]`                          | Disabled options, by key.                                                                                                                                                                                                |
| `value` / `defaultValue`                                                        | — (properties)                              | `readonly string[]`      | `[]`                          | The selected keys; at most one in single mode. Keys with no item stay in it and show nothing.                                                                                                                            |
| `selectionMode`                                                                 | `selection-mode`                            | `'single' \| 'multiple'` | `'single'`                    | Multiple mode keeps the list open while options toggle.                                                                                                                                                                  |
| `isOpen` / `defaultOpen`                                                        | — / `default-open`                          | `boolean`                | `false`                       | Whether the list is open. Changes are reported by `openchange`.                                                                                                                                                          |
| `placeholder`                                                                   | `placeholder`                               | `string`                 | —                             | The `value` part's text while nothing is selected. There is no default text.                                                                                                                                             |
| `isDisabled`                                                                    | `disabled`                                  | `boolean`                | `false`                       | The trigger is natively disabled and nothing opens.                                                                                                                                                                      |
| `isReadOnly`                                                                    | `readonly`                                  | `boolean`                | `false`                       | `aria-readonly` on the trigger. The list opens and focus moves, but nothing is selected, from the list or the closed trigger.                                                                                            |
| `isRequired`                                                                    | `required`                                  | `boolean`                | `false`                       | `aria-required` on the trigger and `required` on the hidden `<select>`.                                                                                                                                                  |
| `isInvalid`                                                                     | `invalid`                                   | `boolean`                | —                             | `aria-invalid` on the trigger, and shows the `error` part. Unset in a [Form](/components/form/), the form's errors decide it.                                                                                            |
| `shouldFocusWrap`                                                               | `focus-wrap`                                | `boolean`                | `false`                       | The arrows in the list wrap from the last option to the first.                                                                                                                                                           |
| `positionArea`                                                                  | `position-area`                             | `PositionArea`           | `'block-end span-inline-end'` | Where the popover goes: below the trigger, aligned to its start edge. See [Popover](/components/popover/).                                                                                                               |
| `positionTryFallbacks`                                                          | `position-try-fallbacks`                    | `string`                 | `'flip-block, flip-inline'`   | Where it goes when that doesn't fit.                                                                                                                                                                                     |
| `triggerId`                                                                     | —                                           | `string`                 | generated                     | The trigger's id while it has none of its own.                                                                                                                                                                           |
| `listId`                                                                        | —                                           | `string`                 | generated                     | The list's id while it has none of its own. Option ids are made from the list's id.                                                                                                                                      |
| `labelElementType`                                                              | — (the label's tag)                         | `string`                 | `'span'`                      | The tag you render as the label.                                                                                                                                                                                         |
| `labelledBy`                                                                    | `labelled-by`                               | `string`                 | —                             | Ids of more elements that name the trigger and the list.                                                                                                                                                                 |
| `describedBy`                                                                   | `described-by`                              | `string`                 | —                             | Ids of more elements that describe the trigger.                                                                                                                                                                          |
| `tooltip`                                                                       | `tooltip` (the `<uni-tooltip>`'s id)        | a tooltip's `instance`   | —                             | A [Tooltip](/components/tooltip/) for the trigger. On the element, a `<uni-tooltip>` that `<uni-select>` sits in directly links too. With `shouldOpenOnlyWhenTruncated`, it measures the `value` part, else the trigger. |
| `optionTooltip`                                                                 | `option-tooltip` (the `<uni-tooltip>`'s id) | a tooltip's `instance`   | —                             | One Tooltip for every option (see [Tooltips](#tooltips)). On the element, only the attribute links it.                                                                                                                   |
| `virtualizer`, `itemSize`, `shouldMeasureItems`, `overscan`, `initialItemCount` | as on Listbox                               |                          |                               | Listbox's virtualization, passed to the list.                                                                                                                                                                            |

In a [Form](/components/form/), Select also takes `name`, `form`, `validate`,
`validationTrigger`, `validationDelay`, `validationMessages`, `dependsOn`,
`isEqual` and `shouldPreserve`: see [Field props for the
form](/components/form/#field-props-for-the-form).

The list always focuses options on hover and is vertical; the popover is
always modal and dismissable. Development builds warn when the select has no
name: no `label` part, no `labelledBy` and no `aria-label` on the trigger.

## Events

| Event                | Callback prop          | Detail                                                     | When                                                                                                                                   |
| -------------------- | ---------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `valuechange`        | `onValueChange`        | `readonly string[]`                                        | The user changed the selection, in the list or by typing or ArrowLeft and ArrowRight on the closed trigger. Never for a value you set. |
| `openchange`         | `onOpenChange`         | `boolean`                                                  | The user opened or closed the list. Once per change, never for an `isOpen` you set.                                                    |
| `focuschange`        | `onFocusChange`        | `boolean`                                                  | Focus entered or left the select. The trigger and the popup count as one, so choosing an option reports nothing.                       |
| `renderedkeyschange` | `onRenderedKeysChange` | `{ keys: readonly string[]; sections: readonly string[] }` | The set of options to render changed. Only virtualized lists need it.                                                                  |

## Actions

| Action    | What it does                                                                                                                      |
| --------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `open()`  | Opens the list, as Enter on the trigger does: focus goes to the selected option, else the first one. Does nothing while disabled. |
| `close()` | Closes the list and returns focus to the trigger if it was in the list.                                                           |

In React and Vue they are on the handle (`fruit.actions.open()`); on the
custom element they are methods
(`document.querySelector('uni-select').open()`).
While `isOpen` is controlled, each only reports `openChange` and leaves the
change to you.

## State

| Field                                                 | Type                     | Meaning                                                                   |
| ----------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
| `value`                                               | `readonly string[]`      | The selected keys.                                                        |
| `valueText`                                           | `string`                 | The selected options' text, joined with `, `; `''` when none has an item. |
| `isPlaceholder`                                       | `boolean`                | No selected key has an item: the `value` part shows the placeholder.      |
| `selectionMode`                                       | `'single' \| 'multiple'` | The `selectionMode` prop.                                                 |
| `isOpen`                                              | `boolean`                | The list is open.                                                         |
| `focusedKey`                                          | `string \| null`         | The option with focus, or the one the list focuses when it opens.         |
| `isEmpty`                                             | `boolean`                | The list has no options.                                                  |
| `isDisabled`, `isReadOnly`, `isRequired`, `isInvalid` | `boolean`                | The props of the same names.                                              |
| `isErrorShown`                                        | `boolean`                | The `error` part is shown: invalid and not disabled.                      |
| `isFocusWithin`                                       | `boolean`                | Focus is on the trigger or in the popup.                                  |
| `isFocusVisible`                                      | `boolean`                | That focus came from the keyboard.                                        |
| `isVirtualized`, `renderedKeys`, `renderedSections`   |                          | Listbox's virtualization state.                                           |

Select what you render, and the component renders again only when it changes:

```tsx
const isOpen = useBehaviorState(fruit, (state) => state.isOpen);
```

On the custom element, read `element.instance.getState()`. See
[State](/guide/state/).

## Styling

| Attribute                                                                                              | On                                   | When                                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-uni="select"`                                                                                    | root                                 | Always.                                                                                                                                                    |
| `data-element`                                                                                         | every other part                     | Always.                                                                                                                                                    |
| `data-open`                                                                                            | named parts                          | The list is open.                                                                                                                                          |
| `data-entering`, `data-exiting`                                                                        | named parts                          | The first frames after opening; closing, until your exit animations on the popover end.                                                                    |
| `data-placeholder`                                                                                     | named parts                          | Nothing is selected.                                                                                                                                       |
| `data-multiple`                                                                                        | named parts                          | Multiple mode.                                                                                                                                             |
| `data-disabled`, `data-readonly`, `data-required`, `data-invalid`                                      | named parts                          | The select is disabled, read-only, required or invalid.                                                                                                    |
| `data-focus-within`, `data-focus-visible`                                                              | named parts                          | Focus is on the trigger or in the popup, and from the keyboard.                                                                                            |
| `data-pressed`, `data-hovered`, `data-focused`                                                         | trigger, triggerFrame, triggerExtras | As on [Button](/components/button/). The frame and the extras carry the trigger's, and `data-hovered` is set while the pointer is anywhere over the frame. |
| `data-placement`                                                                                       | popover, arrow                       | The side the popover opened on: `top`, `bottom`, `left` or `right`.                                                                                        |
| `data-empty`, `data-orientation`                                                                       | list, empty                          | Listbox's: no options, and `vertical`.                                                                                                                     |
| `data-selected`, `data-focused`, `data-focus-visible`, `data-hovered`, `data-pressed`, `data-disabled` | option                               | Listbox's. Hovering focuses an option, so `data-focused` follows the mouse too.                                                                            |

"Named parts" are every part except `anchor`, `scroller` and `hiddenSelect`.
State attributes are present or absent, never `"false"`.

| Custom property      | On      | Value                                                                                    |
| -------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `--anchor-width`     | popover | The width of the anchor part, else the trigger frame, else the trigger, for `min-width`. |
| `--available-width`  | popover | The most room the popover has across its positions, in pixels.                           |
| `--available-height` | popover | The same, vertically, for a `max-height` that leaves room to flip.                       |

They resolve on the popover, so use them there:

```css
[data-uni='select'] [data-element='Popover'] {
  min-width: var(--anchor-width);
  max-height: min(16rem, var(--available-height));
  margin: 4px 0; /* the gap from the trigger */
  overflow: auto;
}
```

By default UniAria places the open popover with inline `position: fixed`,
`top` and `left`, and the gap from the trigger is the popover's margin (see
[Popover](/components/popover/) for CSS anchor positioning instead). It also hides
the hidden `<select>` with inline styles. Everything else is your CSS.

### Default styles

The optional [`@uniaria/styles`](/guide/default-styles/) package styles
every part, as in the demo above, whose CSS tab holds only the page's layout.
The trigger is a field's box holding the value, on one line that ends in an
ellipsis, with a soft placeholder and a chevron the styles draw, so leave your
icon out. In a `triggerFrame`, the extras sit over the trigger's end, the value
ends before them, and the chevron comes after them (see
[Labels, frames and extras](/guide/default-styles/#labels-frames-and-extras)).
The popover is a surface a gap away from the trigger, at least as wide as it
and no larger than the room UniAria measures, and the list scrolls inside it;
options are [Listbox](/components/listbox/#default-styles)'s. They cut their text to
one line with an ellipsis, so pair them with an option tooltip (see
[Tooltips](#tooltips)), and their extras come before the check. Opening, the
popover fades in and grows from 90% as it moves 8 pixels away from the
trigger, in 80 milliseconds, and closing reverses it; under reduced motion it
shows and hides at once. The close button stays visually hidden until it has
focus.

## Keyboard

On the closed trigger:

| Key                         | Behavior                                                                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Enter, Space                | Opens the list and focuses the selected option, else the first.                                                           |
| ArrowDown, Alt+ArrowDown    | The same.                                                                                                                 |
| ArrowUp, Alt+ArrowUp        | Opens the list and focuses the selected option, else the last.                                                            |
| ArrowLeft, ArrowRight       | Single mode: selects the previous or next enabled option without opening; with nothing selected, the first. Doesn't wrap. |
| Printable characters        | Single mode: typeahead selects the next matching option without opening. Space while a query is in progress joins it.     |
| Home, End, PageUp, PageDown | Not handled.                                                                                                              |
| Escape                      | Not handled, so it reaches a dialog around the select.                                                                    |

In the open list, focus is on an option and [Listbox's keys](/components/listbox/#keyboard)
apply: the arrows, Home, End, the page keys, typeahead and, in multiple mode,
the range keys. Besides them:

| Key            | Behavior                                                                                                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Enter, Space   | Single mode: selects the option and closes the list. Multiple mode: toggles it.                                                    |
| Escape         | Closes the list without changing the value. Focus returns to the trigger.                                                          |
| Tab, Shift+Tab | Closes the list without selecting, and moves focus on from the trigger: to what follows it, or with Shift to what comes before it. |

Each of the trigger's extras is a Tab stop of its own after the trigger, in
DOM order, so Tab in the open list closes it and moves focus to the first
extra.

## Accessibility

* **The trigger** is a `<button>` with `role="combobox"`,
  `aria-haspopup="listbox"`, `aria-expanded` and `aria-controls`, named by the
  label alone; its text is the value. It reads "combobox, Fruit, has popup
  listbox, not expanded", then "Apple". Because it is a combobox, it can carry
  `aria-required`, `aria-invalid` and `aria-readonly`, which a plain button
  can't. `aria-controls` stays while the list is closed: Chromium computes the
  value from the selected option of the list it points at.
* **Focus moves into the list** when it opens, to the selected option, else
  the first one (the last for ArrowUp), and the list reads "listbox, Fruit".
  The APG's select-only combobox keeps focus on the trigger and moves
  `aria-activedescendant`, which VoiceOver on iOS and TalkBack ignore.
* **While open**, the rest of the page is hidden from assistive technology
  with `aria-hidden`, but it isn't inert: one press outside both closes the
  list and acts on what it hit. Render a visually hidden `close` button for
  touch screen-reader users, who can't reach the page to dismiss it.
* **On close**, focus returns to the trigger as the closing starts, so a key
  pressed during an exit animation doesn't reach an option. After a press
  outside, focus stays where the press put it.
* **The label** isn't a `<label for>`: activating it would open the list. A
  press on it focuses the trigger and shows its focus ring.
* **The hidden `<select>`** has `aria-hidden="true"` and `tabindex="-1"`, so
  screen readers and Tab skip it.
* **The root part** is needed for `focuschange` and the focus attributes.
* **Extras** beside the trigger aren't part of its name or value: each reads
  on its own, such as "button, Clear Fruit".
* **Option extras** describe their option after its description: "option,
  Apple, New, not selected". Extras with `aria-hidden="true"` stay out of it.
* **Tooltips.** An overflow tooltip with `relation: 'none'` adds nothing: a
  cut-off value still reads "combobox, Fruit, has popup listbox, not
  expanded", then the whole value, which is in the trigger already. The option
  tooltip never describes an option.

## Compared with other libraries

* **The trigger is a combobox**, named by the label, as in Radix and the APG.
  React Aria's is a plain button named "Apple Fruit", which can't say
  required, invalid or read-only.
* **Focus moves into the list**, as in React Aria, Radix and the browser's
  customizable select. The APG keeps focus on the trigger and moves
  `aria-activedescendant`.
* **Tab closes the list without selecting** and moves on. The APG selects the
  focused option; React Aria and Radix keep Tab in the popup.
* **Typing on the closed trigger selects** without opening, as native
  `<select>`, React Aria and Radix do. The APG opens the list.
* **A mouse opens the list on press**, and releasing over an option after
  moving 10 px selects it, as native `<select>`, React Aria and Radix do. The
  APG opens on click. Touch opens on release.
* **The value is an array of keys** in both modes, as Listbox's.
* **Clearing and other actions** are your own controls in `triggerExtras`,
  instead of Cube UI Kit's `isClearable` and `actions`. A press on one while
  the list is open closes it and acts; Cube UI Kit keeps it open.
* **Compared with a native `<select>`**, options can hold markup and groups
  can have headings, multiple mode opens a popup, and the hidden `<select>`
  keeps form submission, validation and autofill.
