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="w-full gap-3 items-center justify-center !py-20 grid">
<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>
</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>
<div class="w-full gap-3 items-center justify-center !py-20 grid">
<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>
</div>
</template>Grouped Options
Options split into named groups. #item-prefix renders a colored swatch per row.
Custom Value
Free-form acceptance via allowCustomValue: the typed query becomes the model value when nothing matches, and unknown external values are preserved. The component renders a built-in "Create X" row as a click affordance. Use this when you want a "text input with autocomplete" feel. For richer create-new UX (custom label, icon, persistence callback), see Create New below.
Clearable
Uses the #trigger slot to compose a custom trigger with an inline clear button. The X clears v-model via @click.stop so the popover doesn't toggle, and @pointerdown.stop keeps the anchor from intercepting the press.
Create New
"Create new" is just a type: 'custom' option. condition hides the row when the query is empty or already matches an existing item, and onClick receives the typed query so you can persist the new value.
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>
<div class="w-full gap-3 items-center justify-center !py-20 grid">
<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>
</div>
</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
/**
* Free-form acceptance: the typed query is accepted as the model value
* when nothing matches, and external `modelValue` updates with unknown
* strings are preserved. The combobox also renders a built-in "Create X"
* row as a click affordance.
*
* For richer create-new UX (custom label / icon / persistence callback),
* prefer a `type: 'custom'` option with `condition` instead — see the
* Create New story. The two are independent and can be combined.
*/
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
}
/**
* Shared shape for `#trigger`, `#prefix`, and `#suffix`. `selectedOption`
* is always `null` in `#prefix` because the prefix only renders before a
* selection — the field is still exposed for slot-prop symmetry across
* the trio.
*/
export type ComboboxSlotProps = ComboboxTriggerSlotProps
export type ComboboxPrefixSlotProps = ComboboxSlotProps
export type ComboboxSuffixSlotProps = ComboboxSlotProps
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. Receives the same shape
* as `#trigger` and `#suffix` (`ComboboxSlotProps`). */
'prefix'?: (props: ComboboxPrefixSlotProps) => any
/**
* Content rendered after the input (input mode) or label (button mode).
* Providing this slot **replaces the default chevron** — render your
* own fallback (e.g. the chevron) when your slot content is conditional.
* Common use: an inline clear button. Use `@click.stop` and
* `@pointerdown.stop` so the press doesn't toggle the popover.
*/
'suffix'?: (props: ComboboxSuffixSlotProps) => 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 | true | 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 Free-form acceptance: the typed query is accepted as the model value when nothing matches, and external `modelValue` updates with unknown strings are preserved. The combobox also renders a built-in "Create X" row as a click affordance. For richer create-new UX (custom label / icon / persistence callback), prefer a `type: 'custom'` option with `condition` instead — see the Create New story. The two are independent and can be combined. |
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 | ComboboxTriggerSlotProps Content rendered before the default input. Receives the same shape as `#trigger` and `#suffix` (`ComboboxSlotProps`). |
suffix | ComboboxTriggerSlotProps Content rendered after the input (input mode) or label (button mode). Providing this slot **replaces the default chevron** — render your own fallback (e.g. the chevron) when your slot content is conditional. Common use: an inline clear button. Use `@click.stop` and `@pointerdown.stop` so the press doesn't toggle the popover. |
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] |