Select
Select provides an anchored menu for choosing one or many options, with optional icons, filtering, and validation text.
Field spacing belongs to the surrounding layout; preserve its intended density instead of normalizing every field group to one gap. labelClearance accepts a CSS length for local block-start clearance and can inherit a subtree default from fieldContextKey; it adds to any parent gap. See the field layout guide.
Anatomy
| Element | Part | Selector |
|---|---|---|
<div> | root | [data-scope="select"][data-part="root"]LMBCopy |
<div> | trigger | [data-scope="select"][data-part="trigger"]LMBCopy |
<button> | clear-buttonconditional | [data-scope="select"][data-part="clear-button"]LMBCopy |
<Icon> | clear-icon | [data-scope="select"][data-part="clear-icon"]LMBCopy |
<span> | indicator | [data-scope="select"][data-part="indicator"]LMBCopy |
<Menu> | menu | [data-scope="select"][data-part="menu"] |
<TransitionGroup> | options | [data-scope="select"][data-part="options"]LMBCopy |
<div> | emptyconditional | [data-scope="select"][data-part="empty"]LMBCopy |
<MenuItem> | option | [data-scope="select"][data-part="option"]LMBCopy |
<span> | option-leading-content | [data-scope="select"][data-part="option-leading-content"]LMBCopy |
<span> | option-checkconditional | [data-scope="select"][data-part="option-check"]LMBCopy |
<span> | option-iconconditional | [data-scope="select"][data-part="option-icon"]LMBCopy |
Props
| Name | Description | Default | Control |
|---|---|---|---|
shapeOptional | Corner style; inherits the shared shape default. M3Shape | undefined | - | - |
labelOptional | Floating label, or accessible-name fallback in external mode. Otherwise supply an external label or ARIA name. string | undefined | undefined | |
labelBehaviorOptional | Floating label or externally arranged visible label; label remains an accessible-name fallback. FieldLabelBehavior | undefined | - | - |
idOptional | Optional explicit id for the trigger element. string | undefined | undefined | |
optionsRequired | Selectable options rendered in the menu. SelectOption[] | - | - |
filterModeOptional | Consumer-owned search results bypass local filtering. localexternal | - | |
resolveLabelOptional | Resolve selected values absent from current results; reactive caches are supported. ((value: SelectOptionValue) => string | undefined) | undefined | - | |
supportingTextOptional | Supporting text shown below the field when not in error state. string | undefined | undefined | |
errorOptional | Error text shown below the field. When provided, field is invalid. string | undefined | undefined | |
disabledOptional | Whether the control is disabled. boolean | undefined | false | |
requiredOptional | Whether the control is required. boolean | undefined | false | |
nameOptional | Name used for hidden inputs during form submission. string | undefined | undefined | |
placeholderOptional | Placeholder shown when nothing is selected. string | undefined | undefined | |
sizeOptional | The trigger height. Matches the 32px, 40px, and 56px TextField and Button sizes. TextFieldSize | undefined | undefined | - |
labelClearanceOptional | CSS length reserved at block-start for the floating label. Overrides FieldContext. string | undefined | undefined | |
multipleOptional | Enables multi-select behavior. boolean | undefined | false | - |
showSelectedIconOptional | Show selected checkmarks; inherits Select defaults, then falls back to true. boolean | undefined | undefined | |
clearableOptional | Shows a trailing button that clears the current selection. boolean | undefined | false | |
clearLabelOptional | Accessible label for the clear button. string | undefined | undefined | |
menuColorOptional | Visual color style of the dropdown menu. MenuColor | undefined | undefined | - |
displaySeparatorOptional | Separator used when showing multiple selected labels. string | undefined | ", " | |
filterableOptional | Enables typing in the field to filter the option collection. boolean | undefined | false | |
filterPlaceholderOptional | Placeholder shown in the field while filtering. string | undefined | undefined | |
filterNoResultsTextOptional | Message shown when filtering returns no matching options. string | undefined | undefined | |
modelValueOptional | - SelectModelValue | - | - |
queryOptional | - string | undefined | "" | |
openOptional | - boolean | undefined | false |
Events7
| Name | Type | Description |
|---|---|---|
update:open | [value: boolean] | - |
closed | [] | - |
update:modelValue | [value: SelectModelValue] | - |
update:query | [value: string] | - |
change | [value: SelectModelValue] | - |
focus | [event: FocusEvent] | - |
blur | [event: FocusEvent] | - |
Slots7
| Name | Parameters | Description |
|---|---|---|
list | - | - |
leading | - | - |
prefix | - | - |
suffix | - | - |
trailing | - | - |
optionLeading | - | - |
optionTrailing | - | - |
Labels follow the shared fieldLabelBehavior default (floating, or external in compact). Override it with label-behavior. External mode omits the visible label and notch, ignores labelClearance, and uses label only as an accessible-name fallback. See Field layout for custom label layouts.
Menu Appearance
menuColor overrides the shared Menu color preference, with standard as the fallback. Configure the plugin's menuColor or provide menuDefaultsContextKey to share a palette across menus. See Menu.
Selected checkmarks
Set :show-selected-icon="false" on Select or Combobox to hide checkmarks while keeping the selected background, leading content, and selection semantics. Rows without leading content reserve no checkmark space and their text does not shift on selection. This also applies to SelectItem rows in a custom list slot.
For an application default, use app.use(M3VuePlugin, { selectDefaults: { showSelectedIcon: false } }). For a subtree, call provideSelectDefaults({ showSelectedIcon: ref(false) }) in setup. The public selectContextKey is also available for direct provision. Precedence is instance prop → subtree defaults → application defaults → true. provideSelectDefaults inherits omitted fields; refs remain reactive and setting a preference or prop to undefined restores inheritance.
External virtual lists
The list slot replaces only the option rendering. Select retains the Menu surface, scroll container, selection, filtering, keyboard navigation, and close lifecycle. Render SelectItem with each entry's option and its index in the filtered entries, keyed by option.value. Values must be unique and stable. SelectItem uses the same MenuItem presentation as the built-in list, including icons, selection motion and endpoint shapes. Its default, leading, and trailing slots receive { option, selected }.
The slot provides:
entries: the complete filtered collection of{ option, index }; each entry'sindexrefers to the original options.scrollElement: the existing MenuContainer scroll element; use it as the virtualizer's scroll element.open: whether the popup is open; disable measurement while closed.activeIndex: the active index in the filtered collection, independent of selection.setScrollToIndex(callback): register a virtualizer scroll callback and unregister withundefinedon unmount.
Keep the full logical result set in options. Do not pass only the mounted window. The virtualizer owns row measurement and placement; it should not add a second scroll container or selection state. Virtual rows do not use the built-in list's filtering transitions, and remounting a selected row does not replay selection keyframes. Use list for custom lists with either Select or Combobox. TanStack is used only in this example, not by the library.
Shared virtual-list component
<script setup lang="ts">
import { computed, onBeforeUnmount, watch } from 'vue';
import { useVirtualizer } from '@tanstack/vue-virtual';
import { SelectItem, type SelectListSlot } from '@/components/Select';
const props = defineProps<{ list: SelectListSlot }>();
const virtualizer = useVirtualizer(
computed(() => ({
count: props.list.entries.length,
getScrollElement: () => props.list.scrollElement ?? null,
estimateSize: () => 48,
getItemKey: (index: number) => props.list.entries[index]!.option.value,
overscan: 5,
enabled: props.list.open,
})),
);
watch(
() => props.list.setScrollToIndex,
(register) => {
register((index) => virtualizer.value.scrollToIndex(index, { align: 'auto' }));
},
{ immediate: true },
);
onBeforeUnmount(() => props.list.setScrollToIndex(undefined));
</script>
<template>
<div class="virtual-select-list" :style="{ height: `${virtualizer.getTotalSize()}px` }">
<SelectItem
v-for="row in virtualizer.getVirtualItems()"
:key="list.entries[row.index]!.option.value"
:ref="
(instance) => {
if (instance) virtualizer.measureElement((instance as any).$el);
}
"
:data-index="row.index"
:index="row.index"
:option="list.entries[row.index]!.option"
class="virtual-select-row"
:style="{ transform: `translateY(${row.start}px)` }"
/>
</div>
</template>
<style scoped>
.virtual-select-list {
flex: none;
position: relative;
inline-size: 100%;
}
.virtual-select-row {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
inline-size: 100%;
}
</style>Search results and selected labels
v-model:query exposes the filter text. Local filtering is the default. With filterMode="external", the caller loads and supplies the complete current result set; it owns request cancellation and stale-response handling. resolveLabel(value) supplies a label for a selected value absent from those results. Read a reactive label cache in this synchronous callback; update that cache when asynchronous data arrives. Missing labels fall back to the string value. Clearing and form submission continue to use the selected values, independent of mounted rows.
Keyboard and accessibility
The field has combobox semantics and controls a listbox containing option rows. Focus remains in the field. Arrow keys navigate the full enabled collection with wrapping; Home/End move to its endpoints while open. Enter selects the active option; Space also selects for non-filterable Select. Escape and Tab close the popup. Navigation does not select an option. aria-activedescendant is present only while the active row is mounted. Consumers migrating custom DOM queries should use these roles instead of the previous menu item roles.
Close lifecycle
update:open(false) updates the control state as soon as the native menu closes. closed follows after the menu's visual exit completes. Filter state is retained through the exit and reset at closed, so the closing menu does not change its option list or dimensions mid-animation.
Examples
Sizes and toolbar alignment
Select and Combobox use the same extra-small (32px), small (40px), and medium (56px) trigger sizes as TextField. The size prop overrides fieldContextKey's size, with medium as the fallback. Configure fieldSize through M3VuePlugin or provide the context for a subtree. See component defaults.
Default
Without Icons
Filtering
Configurations
Composable
useSelect
Full type signature
function useSelect(opts: UseSelectOptions): { filterQuery: import("vue").Ref<string, string>; selectedOptions: import("vue").ComputedRef<UseSelectOption[]>; hasValue: import("vue").ComputedRef<boolean>; displayText: import("vue").ComputedRef<string>; submittedValues: import("vue").ComputedRef<string[]>; filteredOptionEntries: import("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]>; hasFilteredOptions: import("vue").ComputedRef<boolean>; activeIndex: import("vue").ComputedRef<number>; activeDescendant: import("vue").ComputedRef<string | undefined>; list: import("vue").ComputedRef<{ entries: import("./selectModel").SelectOptionEntry[]; scrollElement: HTMLElement | undefined; activeIndex: number; open: boolean; setScrollToIndex: (callback: ((index: number) => void) | undefined) => void; }>; optionProps: (index: number, option: UseSelectOption) => { id: string; 'aria-posinset': number; 'aria-setsize': number; 'data-active': string | undefined; 'data-keyboard-active': string | undefined; 'data-first': string | undefined; 'data-last': string | undefined; ref: (instance: Element | ComponentPublicInstance | null) => void; onMousedown: (event: MouseEvent) => void; onPointermove: () => void; onClick: () => void; }; isOptionSelected: (option: UseSelectOption) => boolean; toggleOption: (option: UseSelectOption) => void; clearValue: () => void; focus: () => void; blur: () => void; show: () => Promise<void>; hide: () => void; toggle: () => void; openMenu: (target?: "first" | "last") => void; closeMenu: () => void; handleFieldClick: () => void; handleFieldInput: () => void; handleFilterKeydown: (event: KeyboardEvent) => void; handleMenuClosed: () => void; }Source: src/composables/useSelect.ts
| Field | Type | Required | Description | Default |
|---|---|---|---|---|
options | () => UseSelectOption[] | Yes | Unique, stable values identify the complete logical result set. | - |
multiple | () => boolean | Yes | - | - |
filterable | () => boolean | Yes | - | - |
disabled | () => boolean | Yes | - | - |
displaySeparator | () => string | Yes | - | - |
locale | (() => string | undefined) | undefined | No | - | - |
filterMode | (() => "local" | "external") | undefined | No | Disable local filtering when the consumer supplies search results. | - |
resolveLabel | ((value: UseSelectOptionValue) => string | undefined) | undefined | No | Resolve values missing from current results. Falls back to String(value). | - |
query | Ref<string, string> | undefined | No | - | - |
modelValue | Ref<UseSelectModelValue, UseSelectModelValue> | Yes | - | - |
open | Ref<boolean, boolean> | Yes | - | - |
onChange | ((value: UseSelectModelValue) => void) | undefined | No | - | - |
onClosed | (() => void) | undefined | No | - | - |
id | () => string | Yes | Stable listbox id, normally derived from useId(). | - |
focus | () => void | Yes | - | - |
blur | () => void | Yes | - | - |
showMenu | () => void | Yes | Use the shared Menu overlay, including source anchoring and close completion. | - |
hideMenu | () => void | Yes | - | - |
scrollElement | () => HTMLElement | undefined | Yes | The MenuContainer is the single scroll owner. | - |
| Field | Type | Required | Description | Default |
|---|---|---|---|---|
filterQuery | import("vue").Ref<string, string> | Yes | - | - |
selectedOptions | import("vue").ComputedRef<UseSelectOption[]> | Yes | - | - |
hasValue | import("vue").ComputedRef<boolean> | Yes | - | - |
displayText | import("vue").ComputedRef<string> | Yes | - | - |
submittedValues | import("vue").ComputedRef<string[]> | Yes | - | - |
filteredOptionEntries | import("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]> | Yes | - | - |
hasFilteredOptions | import("vue").ComputedRef<boolean> | Yes | - | - |
activeIndex | import("vue").ComputedRef<number> | Yes | - | - |
activeDescendant | import("vue").ComputedRef<string | undefined> | Yes | - | - |
list | import("vue").ComputedRef<{ entries: import("./selectModel").SelectOptionEntry[]; scrollElement: HTMLElement | undefined; activeIndex: number; open: boolean; setScrollToIndex: (callback: ((index: number) => void) | undefined) => void; }> | Yes | - | - |
optionProps | (index: number, option: UseSelectOption) => { id: string; 'aria-posinset': number; 'aria-setsize': number; 'data-active': string | undefined; 'data-keyboard-active': string | undefined; 'data-first': string | undefined; 'data-last': string | undefined; ref: (instance: Element | ComponentPublicInstance | null) => void; onMousedown: (event: MouseEvent) => void; onPointermove: () => void; onClick: () => void; } | Yes | - | - |
isOptionSelected | (option: UseSelectOption) => boolean | Yes | - | - |
toggleOption | (option: UseSelectOption) => void | Yes | - | - |
clearValue | () => void | Yes | - | - |
focus | () => void | Yes | - | - |
blur | () => void | Yes | - | - |
show | () => Promise<void> | Yes | - | - |
hide | () => void | Yes | - | - |
toggle | () => void | Yes | - | - |
openMenu | (target?: "first" | "last") => void | Yes | - | - |
closeMenu | () => void | Yes | - | - |
handleFieldClick | () => void | Yes | - | - |
handleFieldInput | () => void | Yes | - | - |
handleFilterKeydown | (event: KeyboardEvent) => void | Yes | - | - |
handleMenuClosed | () => void | Yes | - | - |