Select
Select renders a trigger and a role="listbox" popup. It supports single or multiple values, labels, separators, typeahead, cancelable changes, and Field metadata without a nested subcomponent tree.
Popup placement flips when preferred side lacks room, clamps to viewport, and follows anchor/content resize and ancestor scrolling.
Use it when the user must choose from a known list. Use autocomplete when the same select needs lightweight filtering. Use Combobox when the text input itself is the primary interaction.
Anatomy
#trigger: Custom trigger content. UsetriggerPropswhen rendering your own interactive trigger.#value: Selected value content inside the trigger.#option: Content inside each generatedrole="option"row.#group-label: Content for non-selectable label rows.
The trigger, listbox, options, indicator, separators, and hidden native select are generated by the component. Style those generated parts with the matching ui keys.
Usage
Selected: pro
Open the list and type to filter suggestions.
Selected: none | Search: empty
Examples
Multiple selection
Set multiple and bind v-model to string[]. Selected options stay open so users can toggle more than one item.
<script setup lang="ts">
import { ref } from "vue";
import { Select } from "akaza-ui";
const permissions = ref(["read"]);
const options = [
{ value: "read", label: "Read" },
{ value: "write", label: "Write" },
{ value: "deploy", label: "Deploy" },
];
</script>
<template>
<Select v-model="permissions" multiple :options="options" />
</template>
Grouped options
Use type: "label" and type: "separator" for non-selectable rows. They are skipped by keyboard navigation.
<template>
<Select
v-model="role"
:options="[
{ type: 'label', label: 'Project' },
{ value: 'viewer', label: 'Viewer' },
{ value: 'editor', label: 'Editor' },
{ type: 'separator' },
{ type: 'label', label: 'Admin' },
{ value: 'owner', label: 'Owner' },
]"
/>
</template>
Autocomplete filtering
Set autocomplete to render a search input inside the popup. Focus moves into the search input when opened; that input owns aria-activedescendant and Arrow/Enter/Escape navigation. Bind v-model:search when search text should drive async loading or external filtering.
<script setup lang="ts">
import { ref } from "vue";
const value = ref("");
const search = ref("");
</script>
<template>
<Select
v-model="value"
v-model:search="search"
autocomplete
:options="options"
search-placeholder="Filter options"
/>
</template>
Field integration
Place Select inside Field to inherit id, name, required, disabled state, invalid state, and aria-describedby. Required/native invalid state is revealed after selection, blur, or invalid submit; it is not shown on initial render.
<template>
<Field label="Server region" name="region" required>
<Select v-model="region" :options="regions" placeholder="Choose region" />
</Field>
</template>
Custom option row
Use #option to render richer option content. The component still owns role="option", selection state, highlighting, and click handling.
<template>
<Select v-model="region" :options="regions">
<template #option="{ label, description, isSelected }">
<span aria-hidden="true">{{ isSelected ? "✓" : "" }}</span>
<span>
<strong>{{ label }}</strong>
<small>{{ description }}</small>
</span>
</template>
</Select>
</template>
Cancel a selection
value-change fires before the model changes. Call details.cancel() to block a value.
<template>
<Select
v-model="role"
:options="roles"
@value-change="(value, details) => value === 'owner' && details.cancel()"
/>
</template>
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectOption[] | required | Options rendered in the listbox. |
valueKey | string | — | Object key used as option value. Falls back to value, then label. |
labelKey | string | "label" | Object key used as option label. |
descriptionKey | string | "description" | Object key used as option description. |
disabledKey | string | "disabled" | Object key used as option disabled state. |
placeholder | string | "Select option" | Text shown when no value is selected. |
multiple | boolean | false | Enables multi-select. v-model should be string[]. |
nullableValue | string | "" | Native empty option value for single select. |
id | string | field id | Trigger id. |
name | string | field name | Form field name. Renders a hidden native select. |
required | boolean | false | Required state. |
disabled | boolean | false | Disables trigger and options. |
invalid | boolean | false | Sets invalid attrs. |
loop | boolean | true | Arrow navigation wraps. |
autocomplete | boolean | false | Renders a search input in the popup and filters visible options. |
filter | (option, search) => boolean | label/description contains search | Custom autocomplete filter. |
loading | boolean | false | Shows loading slot/content inside the popup. |
emptyLabel | string | "No options found." | Default empty state text. |
searchPlaceholder | string | "Search options..." | Search input placeholder. |
resetSearchOnSelect | boolean | false | Clears search after selection. |
highlightOnHover | boolean | true | Pointer hover updates highlighted option. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Popup side. |
align | "start" | "center" | "end" | "start" | Popup alignment. |
sideOffset | number | 6 | Popup offset in px. |
ariaLabel | string | — | Accessible label when no visible label exists. |
ariaLabelledby | string | — | Accessible label id. |
ariaDescribedby | string | field description/error | Accessible description ids. |
ui | SelectUi | — | Classes for structural parts. |
Emits
| Event | Payload | Description |
|---|---|---|
open-change | (open, details) | Fired before popup opens/closes. details.cancel() prevents the change. |
value-change | (value: string | string[], details) | Fired before selected value updates. details.cancel() prevents the update. |
search-change | (value: string, details) | Fired before autocomplete search updates. details.cancel() prevents the update. |
Slots
| Slot | Props | Description |
|---|---|---|
trigger | isOpen, selectedOption, selectedOptions, selectedValue, selectedValues, selectedLabel, placeholder, triggerProps, open, close, toggle | Custom trigger content. |
value | option, options, value, values, label | Custom selected value. |
option | option, value, label, description, isSelected, isHighlighted, isDisabled, select | Custom option row. |
group-label | option, label | Custom label row for type: "label" options. |
empty | — | Empty state for autocomplete filtering. |
loading | — | Loading state content. |
Option Shape
| Key | Type | Description |
|---|---|---|
type | "item" | "label" | "separator" | Row type. Omit for selectable items. |
value | string | number | Submitted value for selectable items. |
label | string | Visible label. |
description | string | Optional description. |
disabled | boolean | Disables a selectable item. |
UI Options
| Key | Description |
|---|---|
root | Root wrapper. |
nativeSelect | Hidden native select used for form submission. |
trigger | Trigger button. |
value | Selected value wrapper. |
placeholder | Placeholder text. |
icon | Default chevron icon. |
searchInput | Autocomplete search input. |
content | Listbox popup. |
viewport | Scrollable viewport inside popup. |
empty | Empty state. |
loading | Loading state. |
groupLabel | Non-selectable label row. |
separator | Separator row. |
option | Option row. |
indicator | Selected indicator inside an option. |
optionText | Option text wrapper. |
optionLabel | Option label. |
optionDescription | Option description. |
Styling Hooks
| UI key | CSS class | Data attrs |
|---|---|---|
root | akaza-select | data-akaza-state, data-akaza-side, data-akaza-align, data-akaza-disabled, data-akaza-invalid, data-akaza-filled, data-akaza-focused |
nativeSelect | akaza-select-native | — |
trigger | akaza-select-trigger | Same select state attrs plus ARIA combobox attrs via trigger props |
value | akaza-select-value | — |
placeholder | akaza-select-placeholder | — |
icon | akaza-select-icon | — |
searchInput | akaza-select-search-input | role="searchbox", aria-controls, aria-activedescendant, aria-expanded |
content | akaza-select-content | data-akaza-state, data-akaza-side, data-akaza-align, --akaza-select-anchor-width, --akaza-select-anchor-height, --akaza-select-available-width, --akaza-select-available-height, --akaza-select-transform-origin, --akaza-select-duration |
viewport | akaza-select-viewport | — |
empty | akaza-select-empty | — |
loading | akaza-select-loading | — |
groupLabel | akaza-select-group-label | — |
separator | akaza-select-separator | — |
option | akaza-select-option | data-akaza-state, data-akaza-highlighted, data-akaza-disabled, aria-disabled |
indicator | akaza-select-indicator | — |
optionText | akaza-select-option-text | — |
optionLabel | akaza-select-option-label | — |
optionDescription | akaza-select-option-description | — |
Plain class applies to the root wrapper. Use ui.trigger, ui.content, and ui.option for internal parts.
Popup entry and exit use a subtle side-aware structural transition. Override --akaza-select-duration to change its 120ms duration. Reduced-motion preference shortens it automatically.
Native data-akaza-invalid appears after interaction or invalid submit. Controlled invalid state from the invalid prop or parent Field appears immediately.
Keyboard
| Key | Behavior |
|---|---|
Enter / Space | Open select, or select/toggle highlighted option. |
ArrowDown / ArrowUp | Open select and move highlight. |
Home / End | Move highlight to first/last enabled option. |
Escape | Close popup. |
| printable character | Typeahead to matching option label. |