MultiSelect

Searchable multi-choice picker. Matches the Combobox / Select item-slot model and provides built-in Clear All / Select All footer controls.

Default

A plain picker — button trigger opens a popover with a search input, option list, and default footer.

Item Prefix

Use #item-prefix to render avatars, icons, or indicators next to each option label.

Grouped Options

Options can be split into named groups. Group labels render above each group's items.

Replace the default Clear All / Select All footer with a custom one. The slot receives clearAll, selectAll, selectedOptions, and query.

Custom Trigger

Use #trigger to fully replace the default button trigger. The slot receives open, disabled, selectedOptions, displayValue, clearAll, and toggleOpen.

Tags Trigger

A chips-style trigger: each selected option renders as a removable Badge, with inline remove buttons. Authored through #trigger using selectedOptions and the parent's v-model.

Label, Description, Error

MultiSelect supports label, description, error, and required directly — no FormControl wrapper needed. The error suppresses the description and wires aria-invalid + aria-errormessage onto the trigger.

Pick as many as you like.

API Reference

Show types
typescript
import type { Component, VNode, VNodeChild } from 'vue'
import type { InputLabelingProps } from '../../composables/useInputLabeling'

export type MultiSelectVariant = 'subtle' | 'outline' | 'ghost'
export type MultiSelectSize = 'sm' | 'md' | 'lg' | 'xl'

export type PopoverSide = 'top' | 'right' | 'bottom' | 'left'
export type PopoverAlign = 'start' | 'center' | 'end'

export type MultiSelectSlotFn<TProps> = (props: TProps) => VNodeChild

export interface MultiSelectItemSlots<TProps> {
  /** Replaces the prefix region of the standard row shell. */
  prefix?: MultiSelectSlotFn<TProps>

  /** Replaces the label region of the standard row shell. */
  label?: MultiSelectSlotFn<TProps>

  /** Replaces the suffix region of the standard row shell. */
  suffix?: MultiSelectSlotFn<TProps>

  /** Replaces the entire row; mutually exclusive with `prefix` / `label` / `suffix`. */
  item?: MultiSelectSlotFn<TProps>
}

export interface MultiSelectOption {
  label: string
  value: string
  icon?: string | Component
  description?: string
  disabled?: boolean
  slot?: string
  /** Per-item inline slot implementations for the row shell. */
  slots?: MultiSelectItemSlots<MultiSelectItemSlotProps>
  /** @deprecated use `slot` */
  slotName?: string
  /** @deprecated use `slots` — function form maps to `slots.item`, object form to `slots` */
  render?:
    | (() => VNode | VNode[])
    | MultiSelectItemSlots<MultiSelectItemSlotProps>
  [key: string]: any
}

export interface MultiSelectGroupedOption {
  key?: string | number
  group: string
  hideLabel?: boolean
  options: MultiSelectOption[]
}

export type MultiSelectOptions = Array<
  MultiSelectOption | MultiSelectGroupedOption
>

export interface MultiSelectProps extends InputLabelingProps {
  /** Array of selected option values. */
  modelValue?: string[]

  /** Options rendered in the popover. */
  options?: MultiSelectOptions

  /** Visual style of the trigger. */
  variant?: MultiSelectVariant

  /** Size of the trigger and option rows. */
  size?: MultiSelectSize

  /** Placeholder text shown when no value is selected. */
  placeholder?: string

  /** Disables the multi-select. */
  disabled?: boolean

  /** Controls the popover visibility. */
  open?: boolean

  /** Hides the in-popover search input. */
  hideSearch?: boolean

  /** Replaces the results with a loading state. */
  loading?: boolean

  /** Fallback empty-state copy. */
  emptyText?: string

  /** Preferred popover side. */
  side?: PopoverSide

  /** Preferred popover alignment. */
  align?: PopoverAlign

  /** Gap between trigger and content. */
  offset?: number

  /** Teleport target for the popover content. */
  portalTo?: string | HTMLElement

  /**
   * Custom equality function used to resolve which options are currently
   * selected for display and rendering. When omitted, the component uses
   * strict equality on `option.value` against entries in `modelValue`.
   */
  compareFn?: (a: MultiSelectOption, b: MultiSelectOption) => boolean
}

export interface MultiSelectTriggerSlotProps {
  /** Whether the popover is open. */
  open: boolean

  /** Whether the multi-select is disabled. */
  disabled: boolean

  /** Resolved option objects for the selected values, in `modelValue` order. */
  selectedOptions: MultiSelectOption[]

  /** Comma-joined labels of the selected options, or `''` when nothing is selected. */
  displayValue: string

  /** Clears all selected values. */
  clearAll: () => void

  /** Toggles the popover open state. */
  toggleOpen: () => void
}

export interface MultiSelectItemSlotProps {
  /** Item currently being rendered. */
  item: MultiSelectOption

  /** Current search query — empty when the user hasn't typed since opening. */
  query: string

  /** Whether the item is in `modelValue`. */
  selected: boolean
}

export interface MultiSelectGroupLabelSlotProps {
  /** Group currently being rendered. */
  group: MultiSelectGroupedOption
}

export interface MultiSelectEmptySlotProps {
  /** Current search query — empty when the user hasn't typed since opening. */
  query: string
}

export interface MultiSelectFooterSlotProps {
  /** Clears all selected values. */
  clearAll: () => void

  /** Selects every enabled option across all groups. */
  selectAll: () => void

  /** Resolved option objects for the selected values, in `modelValue` order. */
  selectedOptions: MultiSelectOption[]

  /** Current search query — empty when the user hasn't typed since opening. */
  query: string
}

export interface MultiSelectSlots {
  /** Fully custom trigger renderer. */
  trigger?: (props: MultiSelectTriggerSlotProps) => any

  /** Overrides the rendered label content. Receives `{ required }`. */
  label?: (props: { required: boolean }) => any

  /** Overrides the rendered description content. */
  description?: () => any

  /** Shared content rendered before the standard row label. */
  'item-prefix'?: (props: MultiSelectItemSlotProps) => any

  /** Shared content rendered for the standard row label area. */
  'item-label'?: (props: MultiSelectItemSlotProps) => any

  /** Shared content rendered after the standard row label area. */
  'item-suffix'?: (props: MultiSelectItemSlotProps) => any

  /** Replaces the entire row. */
  item?: (props: MultiSelectItemSlotProps) => any

  /** Custom renderer for group labels. */
  'group-label'?: (props: MultiSelectGroupLabelSlotProps) => any

  /** Fallback content rendered when there are no results. */
  empty?: (props: MultiSelectEmptySlotProps) => any

  /** Replaces the default Clear All / Select All footer. */
  footer?: (props: MultiSelectFooterSlotProps) => any

  /** @deprecated compatibility alias for `#item-label`. */
  option?: (props: { item: MultiSelectOption }) => any

  [slotName: string]: ((props: any) => any) | undefined
}

export interface MultiSelectEmits {
  /** Fired when the selection changes. */
  'update:modelValue': [value: string[]]

  /** Fired when the open state changes. */
  'update:open': [value: boolean]

  /** Fired when the user types in the search input. */
  'update:query': [value: string]
}
Prop Default Type
modelValue
[]
string[]

Array of selected option values.

options
[]
MultiSelectOptions

Options rendered in the popover.

variant
"subtle"
MultiSelectVariant

Visual style of the trigger.

size
"sm"
MultiSelectSize

Size of the trigger and option rows.

placeholder
"Select option"
string

Placeholder text shown when no value is selected.

disabled
false
boolean

Disables the multi-select.

open
false
boolean

Controls the popover visibility.

hideSearch
false
boolean

Hides the in-popover search input.

loading
false
boolean

Replaces the results with a loading state.

emptyText
"No results"
string

Fallback empty-state copy.

side
"bottom"
PopoverSide

Preferred popover side.

align
"start"
PopoverAlign

Preferred popover alignment.

offset
4
number

Gap between trigger and content.

portalTo
"body"
string | HTMLElement

Teleport target for the popover content.

compareFn
((a: MultiSelectOption, b: MultiSelectOption) => boolean)

Custom equality function used to resolve which options are currently selected for display and rendering. When omitted, the component uses strict equality on `option.value` against entries in `modelValue`.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

Helper text rendered below the input. Hidden when `error` is set.

error
string | FrappeUIError

Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).

required
boolean

Marks the field as required. Renders an asterisk next to the label and forwards `required` / `aria-required` to the underlying control.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

Slot Payload
trigger
MultiSelectTriggerSlotProps

Fully custom trigger renderer.

label
{ required: boolean; }

Overrides the rendered label content. Receives `{ required }`.

description

Overrides the rendered description content.

item-prefix
MultiSelectItemSlotProps

Shared content rendered before the standard row label.

item-label
MultiSelectItemSlotProps

Shared content rendered for the standard row label area.

item-suffix
MultiSelectItemSlotProps

Shared content rendered after the standard row label area.

item
MultiSelectItemSlotProps

Replaces the entire row.

group-label
MultiSelectGroupLabelSlotProps

Custom renderer for group labels.

empty
MultiSelectEmptySlotProps

Fallback content rendered when there are no results.

footer
MultiSelectFooterSlotProps

Replaces the default Clear All / Select All footer.

option

Deprecated — compatibility alias for `#item-label`.

Event Payload
update:modelValue
unknown[]

Fired when the model value changes.

update:query
[value: string]

Fired when the query changes.

update:open
unknown[]

Fired when the open state changes.