GitHubopen_in_new

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

ElementPartSelector
<div>
root
[data-scope="select"][data-part="root"]
left_clickLMBCopy
<div>
trigger
[data-scope="select"][data-part="trigger"]
left_clickLMBCopy
<button>
clear-buttonconditional
[data-scope="select"][data-part="clear-button"]
left_clickLMBCopy
<Icon>
clear-icon
[data-scope="select"][data-part="clear-icon"]
left_clickLMBCopy
<span>
indicator
[data-scope="select"][data-part="indicator"]
left_clickLMBCopy
<Menu>
menu
[data-scope="select"][data-part="menu"]
<TransitionGroup>
options
[data-scope="select"][data-part="options"]
left_clickLMBCopy
<div>
emptyconditional
[data-scope="select"][data-part="empty"]
left_clickLMBCopy
<MenuItem>
option
[data-scope="select"][data-part="option"]
left_clickLMBCopy
<span>
option-leading-content
[data-scope="select"][data-part="option-leading-content"]
left_clickLMBCopy
<span>
option-checkconditional
[data-scope="select"][data-part="option-check"]
left_clickLMBCopy
<span>
option-iconconditional
[data-scope="select"][data-part="option-icon"]
left_clickLMBCopy

Props

NameDescriptionDefaultControl
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

NameTypeDescription
update:open[value: boolean]-
closed[]-
update:modelValue[value: SelectModelValue]-
update:query[value: string]-
change[value: SelectModelValue]-
focus[event: FocusEvent]-
blur[event: FocusEvent]-

Slots7

NameParametersDescription
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.

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's index refers 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 with undefined on 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
vue
<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 ​

Single selection
Multiple selection stays open after each pick.
Selection required

Composable ​

useSelect ​

Full type signaturefunction 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

FieldTypeRequiredDescriptionDefault
options() => UseSelectOption[]YesUnique, stable values identify the complete logical result set.-
multiple() => booleanYes--
filterable() => booleanYes--
disabled() => booleanYes--
displaySeparator() => stringYes--
locale(() => string | undefined) | undefinedNo--
filterMode(() => "local" | "external") | undefinedNoDisable local filtering when the consumer supplies search results.-
resolveLabel((value: UseSelectOptionValue) => string | undefined) | undefinedNoResolve values missing from current results. Falls back to String(value).-
queryRef<string, string> | undefinedNo--
modelValueRef<UseSelectModelValue, UseSelectModelValue>Yes--
openRef<boolean, boolean>Yes--
onChange((value: UseSelectModelValue) => void) | undefinedNo--
onClosed(() => void) | undefinedNo--
id() => stringYesStable listbox id, normally derived from useId().-
focus() => voidYes--
blur() => voidYes--
showMenu() => voidYesUse the shared Menu overlay, including source anchoring and close completion.-
hideMenu() => voidYes--
scrollElement() => HTMLElement | undefinedYesThe MenuContainer is the single scroll owner.-
FieldTypeRequiredDescriptionDefault
filterQueryimport("vue").Ref<string, string>Yes--
selectedOptionsimport("vue").ComputedRef<UseSelectOption[]>Yes--
hasValueimport("vue").ComputedRef<boolean>Yes--
displayTextimport("vue").ComputedRef<string>Yes--
submittedValuesimport("vue").ComputedRef<string[]>Yes--
filteredOptionEntriesimport("vue").ComputedRef<import("./selectModel").SelectOptionEntry[]>Yes--
hasFilteredOptionsimport("vue").ComputedRef<boolean>Yes--
activeIndeximport("vue").ComputedRef<number>Yes--
activeDescendantimport("vue").ComputedRef<string | undefined>Yes--
listimport("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) => booleanYes--
toggleOption(option: UseSelectOption) => voidYes--
clearValue() => voidYes--
focus() => voidYes--
blur() => voidYes--
show() => Promise<void>Yes--
hide() => voidYes--
toggle() => voidYes--
openMenu(target?: "first" | "last") => voidYes--
closeMenu() => voidYes--
handleFieldClick() => voidYes--
handleFieldInput() => voidYes--
handleFilterKeydown(event: KeyboardEvent) => voidYes--
handleMenuClosed() => voidYes--