Combobox
Lets users choose from available options or type their own. Provides clear, responsive feedback for every interaction.
Simple
A plain repo picker — just pass options as an array of strings.
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox } from 'frappe-ui'
const value = ref('frappe-ui')
const repos = [
'gameplan',
'frappe-ui',
'frappe',
'erpnext',
'helpdesk',
'crm',
'wiki',
'insights',
]
</script>
<template>
<div class="grid gap-3">
<Combobox
v-model="value"
:options="repos"
placeholder="Pick a repo"
open-on-focus
/>
<div class="text-sm text-ink-gray-5">
Selected: <code class="text-ink-gray-7">{{ value || 'None' }}</code>
</div>
</div>
</template>Emoji Picker
Button-triggered combobox via trigger="button". The search input moves into the popover header. The button's label and prefix auto-derive from the selected option — #item-prefix doubles as the selected-state prefix, and #prefix is the placeholder icon shown before anything is picked.
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox } from 'frappe-ui'
const value = ref<string>('')
const emojis = [
{
group: 'Smileys',
options: [
{ label: 'Grinning', value: 'grinning', icon: '😀' },
{ label: 'Laughing', value: 'laughing', icon: '😂' },
{ label: 'Heart Eyes', value: 'heart-eyes', icon: '😍' },
{ label: 'Thinking', value: 'thinking', icon: '🤔' },
{ label: 'Mind Blown', value: 'mind-blown', icon: '🤯' },
],
},
{
group: 'Gestures',
options: [
{ label: 'Thumbs Up', value: 'thumbs-up', icon: '👍' },
{ label: 'Clap', value: 'clap', icon: '👏' },
{ label: 'Party', value: 'party', icon: '🎉' },
{ label: 'Rocket', value: 'rocket', icon: '🚀' },
{ label: 'Fire', value: 'fire', icon: '🔥' },
],
},
{
group: 'Objects',
options: [
{ label: 'Sparkles', value: 'sparkles', icon: '✨' },
{ label: 'Bulb', value: 'bulb', icon: '💡' },
{ label: 'Warning', value: 'warning', icon: '⚠️' },
{ label: 'Check', value: 'check', icon: '✅' },
{ label: 'Cross', value: 'cross', icon: '❌' },
],
},
]
</script>
<template>
<Combobox
v-model="value"
trigger="button"
:options="emojis"
placeholder="Pick a reaction"
>
<template #prefix>
<span class="lucide-smile size-4 text-ink-gray-6" />
</template>
</Combobox>
</template>Grouped Options
Options split into named groups. #item-prefix renders a colored swatch per row.
Custom Value
Provides a creatable option if no results match the current query.
Status Picker
Dotted indicator aligned to the first line, with supporting description text.
Member Picker
Avatar rows with a contextual invite action authored through a template slot.
In Dialog
Combobox rendered inside a Dialog. Verifies focus restores to the trigger after the popover closes, even when wrapped by the Dialog's focus scope.
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Combobox, Dialog } from 'frappe-ui'
const open = ref(false)
const repo = ref('frappe-ui')
const reaction = ref('')
const repos = [
'gameplan',
'frappe-ui',
'frappe',
'erpnext',
'helpdesk',
'crm',
'wiki',
'insights',
]
const emojis = [
{
group: 'Smileys',
options: [
{ label: 'Grinning', value: 'grinning', icon: '😀' },
{ label: 'Laughing', value: 'laughing', icon: '😂' },
{ label: 'Heart Eyes', value: 'heart-eyes', icon: '😍' },
{ label: 'Thinking', value: 'thinking', icon: '🤔' },
],
},
{
group: 'Gestures',
options: [
{ label: 'Thumbs Up', value: 'thumbs-up', icon: '👍' },
{ label: 'Party', value: 'party', icon: '🎉' },
{ label: 'Fire', value: 'fire', icon: '🔥' },
],
},
]
</script>
<template>
<Button @click="open = true">Open dialog</Button>
<Dialog v-model="open">
<template #body-title>
<h3 class="text-2xl font-semibold text-ink-gray-9">
Combobox inside Dialog
</h3>
</template>
<template #body-content>
<div class="space-y-4">
<div class="flex flex-col gap-1">
<label class="text-sm text-ink-gray-7">Repository</label>
<Combobox
v-model="repo"
:options="repos"
placeholder="Pick a repo"
open-on-focus
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-sm text-ink-gray-7">Reaction</label>
<Combobox
v-model="reaction"
trigger="button"
:options="emojis"
placeholder="Pick a reaction"
>
<template #prefix>
<span class="lucide-smile size-4 text-ink-gray-6" />
</template>
</Combobox>
</div>
<div class="rounded bg-surface-gray-1 p-3 text-sm text-ink-gray-7">
<div>
Repo: <code>{{ repo || 'None' }}</code>
</div>
<div>
Reaction: <code>{{ reaction || 'None' }}</code>
</div>
</div>
</div>
</template>
<template #actions="{ close }">
<Button variant="solid" @click="close">Done</Button>
</template>
</Dialog>
</template>Label, Description, Error
Combobox supports label, description, error, and required directly — no FormControl wrapper needed. The error suppresses the description and wires aria-invalid + aria-errormessage onto the input.
API Reference
Show types
import type { Component, VNode, VNodeChild } from 'vue'
import type { InputLabelingProps } from '../../composables/useInputLabeling'
export type ComboboxVariant = 'subtle' | 'outline' | 'ghost'
export type ComboboxSize = 'sm' | 'md' | 'lg' | 'xl'
export type PopoverSide = 'top' | 'right' | 'bottom' | 'left'
export type PopoverAlign = 'start' | 'center' | 'end'
/** @deprecated alias for `align` */
export type ComboboxPlacement = PopoverAlign
export type ComboboxSlotFn<TProps> = (props: TProps) => VNodeChild
export interface ComboboxItemSlots<TProps> {
/** Replaces the prefix region of the standard row shell. */
prefix?: ComboboxSlotFn<TProps>
/** Replaces the label region of the standard row shell. */
label?: ComboboxSlotFn<TProps>
/** Replaces the suffix region of the standard row shell. */
suffix?: ComboboxSlotFn<TProps>
/** Replaces the entire row; mutually exclusive with `prefix` / `label` / `suffix`. */
item?: ComboboxSlotFn<TProps>
}
export type ComboboxSelectableOption = {
type?: 'option'
label: string
value: string
icon?: string | Component
description?: string
disabled?: boolean
slot?: string
/** Per-item inline slot implementations for the row shell. */
slots?: ComboboxItemSlots<ComboboxItemSlotProps>
/** @deprecated use `slot` */
slotName?: string
/** @deprecated use `slots` — function form maps to `slots.item`, object form to `slots` */
render?: (() => VNode | VNode[]) | ComboboxItemSlots<ComboboxItemSlotProps>
[key: string]: any
}
export type ComboboxCustomOptionContext = {
query: string
/** @deprecated use `query` */
searchTerm: string
}
export type ComboboxCustomOption = {
type: 'custom'
key: string
label: string
icon?: string | Component
description?: string
disabled?: boolean
slot?: string
/** Per-item inline slot implementations for the row shell. */
slots?: ComboboxItemSlots<ComboboxItemSlotProps>
/** @deprecated use `slot` */
slotName?: string
onClick: (context: ComboboxCustomOptionContext) => void
keepOpen?: boolean
condition?: (context: ComboboxCustomOptionContext) => boolean
/** @deprecated use `slots` — function form maps to `slots.item`, object form to `slots` */
render?: (() => VNode | VNode[]) | ComboboxItemSlots<ComboboxItemSlotProps>
[key: string]: any
}
export type SelectableOption = ComboboxSelectableOption
export type CustomOption = ComboboxCustomOption
export type ComboboxSimpleOption =
| string
| ComboboxSelectableOption
| ComboboxCustomOption
export type SimpleOption = ComboboxSimpleOption
export interface ComboboxGroupedOption {
key?: string | number
group: string
hideLabel?: boolean
options: ComboboxSimpleOption[]
}
export type GroupedOption = ComboboxGroupedOption
export type ComboboxOption = ComboboxSimpleOption | ComboboxGroupedOption
export interface ComboboxProps extends InputLabelingProps {
/** Options rendered in the popover. */
options?: ComboboxOption[]
/**
* Shape of the trigger.
* - `'input'` (default): user types directly into the trigger
* - `'button'`: render a button trigger; search input moves into the
* popover header. Label + prefix auto-derive from the selected option.
*/
trigger?: 'input' | 'button'
/** Visual style of the combobox. */
variant?: ComboboxVariant
/** Size of the trigger and option rows. */
size?: ComboboxSize
/** Placeholder text shown when no value is selected. */
placeholder?: string
/** Disables the combobox. */
disabled?: boolean
/** Controls the popover visibility. */
open?: boolean
/** Opens the popover when the input receives focus. */
openOnFocus?: boolean
/** Opens the popover when the input is clicked. */
openOnClick?: boolean
/** 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
/** Accepts the typed query as the value when nothing matches. */
allowCustomValue?: boolean
/** Replaces the results with a loading state. */
loading?: boolean
/** Fallback empty-state copy. */
emptyText?: string
/**
* Alignment of the popover along the trigger edge.
* @deprecated use `align` instead; `placement` is kept as a back-compat alias
*/
placement?: ComboboxPlacement
}
export interface ComboboxTriggerSlotProps {
/** Whether the popover is open. */
open: boolean
/** Whether the combobox is disabled. */
disabled: boolean
/** Current input query. */
query: string
/** Resolved selected option, if any. */
selectedOption: ComboboxSelectableOption | null
/** Resolved display text for the committed value. */
displayValue: string
}
export interface ComboboxItemSlotProps {
/** Item currently being rendered. */
item: ComboboxSelectableOption | ComboboxCustomOption
/** Current search query — empty when the user hasn't typed since opening. */
query: string
/** Whether the item is selected. */
selected: boolean
}
export interface ComboboxGroupLabelSlotProps {
/** Group currently being rendered. */
group: ComboboxGroupedOption
}
export interface ComboboxEmptySlotProps {
/** Current search query — empty when the user hasn't typed since opening. */
query: string
}
export interface ComboboxSlots {
/** Fully custom trigger renderer. */
trigger?: (props: ComboboxTriggerSlotProps) => any
/** Overrides the rendered label content. Receives `{ required }`. */
label?: (props: { required: boolean }) => any
/** Overrides the rendered description content. */
description?: () => any
/** Content rendered before the default input. */
prefix?: () => any
/** Shared content rendered before the standard row label. */
'item-prefix'?: (props: ComboboxItemSlotProps) => any
/** Shared content rendered for the standard row label area. */
'item-label'?: (props: ComboboxItemSlotProps) => any
/** Shared content rendered after the standard row label area. */
'item-suffix'?: (props: ComboboxItemSlotProps) => any
/** Replaces the entire row. */
item?: (props: ComboboxItemSlotProps) => any
/** Custom renderer for group labels. */
'group-label'?: (props: ComboboxGroupLabelSlotProps) => any
/** Fallback content rendered when there are no results. */
empty?: (props: ComboboxEmptySlotProps) => any
/** Content rendered after the list. */
footer?: () => any
[slotName: string]: ((props: any) => any) | undefined
}
export interface ComboboxEmits {
/** Fired when the open state changes. */
'update:open': [value: boolean]
/** Fired when the query changes due to user input. */
'update:query': [value: string]
/** Fired when the resolved selected option changes. */
'update:selectedOption': [
option: ComboboxSelectableOption | ComboboxCustomOption | null,
]
/** Fired when the input receives focus. */
focus: [event: FocusEvent]
/** Fired when the input loses focus. */
blur: [event: FocusEvent]
/** @deprecated compatibility alias for `update:query`. */
input: [value: string]
}
export interface ComboboxExposed {
reset: () => void
}| Prop | Default | Type |
|---|---|---|
options | [] | ComboboxOption[] Options rendered in the popover. |
trigger | "input" | "button" | "input" Shape of the trigger. - `'input'` (default): user types directly into the trigger - `'button'`: render a button trigger; search input moves into the popover header. Label + prefix auto-derive from the selected option. |
variant | "subtle" | ComboboxVariant Visual style of the combobox. |
size | "sm" | ComboboxSize Size of the trigger and option rows. |
placeholder | "Select option" | string Placeholder text shown when no value is selected. |
disabled | false | boolean Disables the combobox. |
open | — | boolean Controls the popover visibility. |
openOnFocus | false | boolean Opens the popover when the input receives focus. |
openOnClick | false | boolean Opens the popover when the input is clicked. |
side | "bottom" | PopoverSide Preferred popover side. |
align | — | PopoverAlign Preferred popover alignment. |
offset | 4 | number Gap between trigger and content. |
portalTo | "body" | string | HTMLElement Teleport target for the popover content. |
allowCustomValue | false | boolean Accepts the typed query as the value when nothing matches. |
loading | false | boolean Replaces the results with a loading state. |
emptyText | "No results" | string Fallback empty-state copy. |
placement | — | Deprecated — use `align` instead; `placement` is kept as a back-compat alias |
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. |
modelValue | null | string | null |
| Slot | Payload |
|---|---|
trigger | ComboboxTriggerSlotProps Fully custom trigger renderer. |
label | { required: boolean; } Overrides the rendered label content. Receives `{ required }`. |
description | — Overrides the rendered description content. |
prefix | — Content rendered before the default input. |
item-prefix | ComboboxItemSlotProps Shared content rendered before the standard row label. |
item-label | ComboboxItemSlotProps Shared content rendered for the standard row label area. |
item-suffix | ComboboxItemSlotProps Shared content rendered after the standard row label area. |
item | ComboboxItemSlotProps Replaces the entire row. |
group-label | ComboboxGroupLabelSlotProps Custom renderer for group labels. |
empty | ComboboxEmptySlotProps Fallback content rendered when there are no results. |
footer | — Content rendered after the list. |
| Event | Payload |
|---|---|
update:modelValue | [value: string | null] Fired when the model value changes. |
update:query | [value: string] Fired when the query changes. |
input | [value: string] |
update:open | [value: boolean] Fired when the open state changes. |
update:selectedOption | [option: ComboboxSelectableOption | ComboboxCustomOption | null] Fired when the selected option changes. |
focus | [event: FocusEvent] |
blur | [event: FocusEvent] |