Select
A button that opens a list of options and picks one. For a searchable list, use Combobox, and to pick several values, use MultiSelect.
<Select
label="Status"
placeholder="Select status"
:options="options"
v-model="value"
/>Examples
Dessert menu
#item-prefix and #item-label fill the standard row with a photo and a second line of text. #prefix shows the selected option's photo on the trigger.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Avatar, Select } from 'frappe-ui'
const value = ref('matcha-tiramisu')
const options = [
{
label: 'Matcha Tiramisu',
value: 'matcha-tiramisu',
image:
'https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=150&h=150&fit=crop',
description: 'Kyoto cream · Soft sponge · Ceremonial matcha',
price: '$14',
},
{
label: 'Strawberry Cheesecake',
value: 'strawberry-cheesecake',
image:
'https://images.unsplash.com/photo-1533134486753-c833f0ed4866?w=150&h=150&fit=crop',
description: 'Fresh berries · Baked filling · Biscuit crust',
price: '$16',
},
{
label: 'Chocolate Lava Cake',
value: 'chocolate-lava-cake',
image:
'https://images.unsplash.com/photo-1624353365286-3f8d62daad51?w=150&h=150&fit=crop',
description: 'Warm center · Dark chocolate · Sea salt',
price: '$15',
},
{
label: 'Mango Sticky Rice',
value: 'mango-sticky-rice',
image:
'https://images.unsplash.com/photo-1604085792782-8d92f276d7d8?w=150&h=150&fit=crop',
description: 'Coconut cream · Sweet mango · Toasted sesame',
price: '$13',
disabled: true,
},
]
const activeOption = computed(() => {
return options.find((option) => option.value === value.value) ?? null
})
</script>
<template>
<div>
<Select v-model="value" :options="options" variant="outline" class="w-full">
<template #prefix>
<Avatar
v-if="activeOption"
size="sm"
:image="activeOption.image"
:label="activeOption.label"
/>
</template>
<template #item-prefix="{ item }">
<Avatar size="sm" :image="item.image" :label="item.label" />
</template>
<template #item-label="{ item }">
<div class="min-w-0">
<div class="truncate">{{ item.label }}</div>
<div
class="truncate text-p-sm text-ink-gray-5"
:class="item.disabled ? 'opacity-65' : ''"
>
{{ item.description }}
</div>
</div>
</template>
</Select>
</div>
</template>Inline in a sentence
#trigger replaces the whole trigger, so the choice reads as part of the sentence.
<script setup lang="ts">
import { ref } from 'vue'
import { Select } from 'frappe-ui'
const frequency = ref('mentions')
const options = [
{ label: 'for every message', value: 'all' },
{ label: 'for @mentions only', value: 'mentions' },
{ label: 'never', value: 'never' },
]
</script>
<template>
<!--
An inline trigger reads as part of the sentence rather than as a form
control. `#prefix` / `#suffix` can't get here — they add to the default
trigger shell, while `#trigger` replaces it. `variant="ghost"` and
`size="sm"` keep the shell from drawing a border or a tall row.
-->
<p class="max-w-md text-base text-ink-gray-7">
Notify me
<Select
v-model="frequency"
:options="options"
variant="ghost"
size="sm"
class="!px-0"
>
<template #trigger="{ selectedOption, open }">
<span
:class="[
'rounded-4 px-1 -mx-1 font-medium text-ink-gray-8 decoration-outline-gray-3 underline-offset-4 transition-colors duration-150',
open ? 'bg-surface-gray-3' : 'underline hover:bg-surface-gray-3',
]"
>
{{ selectedOption?.label }}
</span>
</template>
</Select>
in this project.
</p>
</template>Timezone picker
The #footer slot sits below the options and stays in place while the list scrolls.
<script setup lang="ts">
import { ref } from 'vue'
import { Select } from 'frappe-ui'
const value = ref('')
// A long list so the viewport scrolls — the footer should stay pinned to the
// bottom of the popover instead of scrolling away with the options.
const timezones = [
'UTC-12:00',
'UTC-11:00',
'UTC-10:00',
'UTC-09:00',
'UTC-08:00',
'UTC-07:00',
'UTC-06:00',
'UTC-05:00',
'UTC-04:00',
'UTC-03:00',
'UTC-02:00',
'UTC-01:00',
'UTC+00:00',
'UTC+01:00',
'UTC+02:00',
'UTC+03:00',
'UTC+04:00',
'UTC+05:00',
'UTC+05:30',
'UTC+06:00',
'UTC+07:00',
'UTC+08:00',
'UTC+09:00',
'UTC+10:00',
'UTC+11:00',
'UTC+12:00',
]
</script>
<template>
<div class="grid gap-3">
<Select v-model="value" :options="timezones" placeholder="Pick a timezone">
<template #footer="{ selectedOption, clear, setOpen }">
<div
class="flex items-center justify-between border-t border-outline-gray-1 px-3 py-2 text-sm text-ink-gray-5"
>
<span>{{ timezones.length }} timezones</span>
<button
v-if="selectedOption"
class="text-ink-gray-7 hover:text-ink-gray-8"
@click="
() => {
clear()
setOpen(false)
}
"
>
Clear
</button>
<span v-else class="text-ink-gray-7">Footer stays fixed</span>
</div>
</template>
</Select>
<div class="text-sm text-ink-gray-5">
Selected: <code class="text-ink-gray-7">{{ value || 'None' }}</code>
</div>
</div>
</template>Behavior
Options
Select takes a flat list of options. It has no groups. Empty and nullish options are left out. Option values are string | number. An option with disabled: true shows in the list but cannot be picked.
Empty value
When nothing is selected, the value is null, the same as in Combobox. An empty string is a real value, so a "None" option with value: '' round-trips.
For a "Sort by" menu, add a first option with value: '' and disabled: true. It shows as the label while nothing else is picked, and people cannot select it.
Width
Select sizes itself to fit its options. The trigger fits the selected value, and the menu grows to fit longer options. Add class="w-full" for a full-width trigger.
Item slots
#item-prefix, #item-label and #item-suffix change parts of the standard row. #item replaces the whole row, including its outer element.
Trigger slots
#trigger replaces the trigger content. For smaller changes, #prefix and #suffix sit inside the default trigger. #suffix replaces the chevron.
#trigger, #prefix, #suffix and #footer all receive { open, disabled, selectedOption, clear, setOpen, close }. close() is the same as setOpen(false).
Open state
Use v-model:open when a parent component controls the menu. Inside #trigger or #footer, use the setOpen slot prop.
Menu placement
By default the menu opens over the trigger, lined up with the selected option. Pass side, align or offset to place it next to the trigger instead. portalTo changes where the menu is teleported in both modes.
Label, description and error
label renders above the trigger and description below it. error renders below the trigger and hides description. It takes a string, an array of strings (one line each), or an Error, the same values as ErrorMessage. An empty string or an empty array means no error. required adds a red asterisk to the label.
The #label slot replaces the label text and the required marker, and receives { required }. A #description slot is not hidden by error. It renders above the error.
Accessibility
While error is set, the trigger gets aria-invalid and an aria-errormessage that points to the error text.
API Reference
Show types
import type { Component } from 'vue'
import type { InputSize, InputVariant } from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'
import type { PopoverAlign, PopoverSide } from '../shared/selection/types'
import type { PortalTarget } from '../../composables/usePortalTarget'
export type { PopoverAlign, PopoverSide }
/**
* Option values are `string | number` across the whole selection family.
* `''` is a legitimate value (a "none" / reset row) and is round-tripped
* through `useEmptyValueMapping`.
*/
export type SelectOptionValue = string | number
export type SelectOption =
| string
| {
label: string
value: SelectOptionValue
disabled?: boolean
icon?: string | Component
description?: string
slot?: string
[key: string]: any
}
export type SelectNormalizedOption = Exclude<SelectOption, string>
export interface SelectProps extends InputLabelingProps {
/** Size of the select input. */
size?: InputSize
/** Visual style of the select input. */
variant?: InputVariant
/** Placeholder text displayed when no option is selected. */
placeholder?: string
/** If true, disables the select input. */
disabled?: boolean
/** The currently selected value. `null` when nothing is selected. */
modelValue?: SelectOptionValue | null
/** Controls the visibility of the select menu. */
open?: boolean
/** Options to display in the dropdown. */
options?: SelectOption[]
/** Fallback empty-state copy rendered when no options are available. */
emptyText?: string
/**
* Preferred popover side. Defaults to `'bottom'`.
*
* Setting `side`, `align`, or `offset` switches the menu from its default
* item-aligned placement (anchored over the trigger, macOS-style) to
* standard popper placement below/beside the trigger.
*/
side?: PopoverSide
/** Preferred popover alignment. Defaults to `'start'`. See `side`. */
align?: PopoverAlign
/** Gap in px between trigger and content. Defaults to `4`. See `side`. */
offset?: number
/** Teleport target for the popover content. Unset, an embedding host's target is used, else `body`. */
portalTo?: PortalTarget
}
/**
* Shared shape for `#trigger`, `#prefix`, `#suffix`, and `#footer`.
* `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 group.
*
* `clear` and `setOpen` are the inside-out helpers: code running inside a
* slot has no reference to the parent's model or open state.
*/
export interface SelectSlotProps {
/** Whether the select menu is currently open. */
open: boolean
/** Whether the trigger is disabled. */
disabled: boolean
/** Currently selected option, if any. */
selectedOption: SelectNormalizedOption | null
/** Clears the current selection (sets the model to `null`). */
clear: () => void
/** Sets the menu open state. */
setOpen: (value: boolean) => void
/** Closes the menu. Equivalent to `setOpen(false)`. */
close: () => void
}
export type SelectTriggerSlotProps = SelectSlotProps
export type SelectPrefixSlotProps = SelectSlotProps
export type SelectSuffixSlotProps = SelectSlotProps
export interface SelectItemSlotProps {
/** Item currently being rendered. */
item: SelectNormalizedOption
/** Whether the item is the current `modelValue`. */
selected: boolean
}
/**
* Fixed slot names. Kept separate from `SelectSlots` so the dynamic
* `` `item-${string}` `` index signature can be intersected in without
* constraining names that don't match the pattern.
*/
interface SelectFixedSlots {
/** Fully custom trigger renderer. */
trigger?: (props: SelectTriggerSlotProps) => any
/** Overrides the rendered label content. Receives `{ required }`. */
label?: (props: { required: boolean }) => any
/** Overrides the rendered description content. */
description?: () => any
/** Content rendered before the trigger value. Receives the same shape
* as `#trigger` and `#suffix` (`SelectSlotProps`). */
prefix?: (props: SelectPrefixSlotProps) => any
/**
* Content rendered after the trigger value. Providing this slot
* **replaces the default chevron** — render your own fallback when
* your slot content is conditional.
*/
suffix?: (props: SelectSuffixSlotProps) => any
/**
* Replaces the entire option row, including the row shell. A per-option
* `slot` (`#item-<name>`) takes precedence over this slot.
*/
item?: (props: SelectItemSlotProps) => any
/** Fallback content rendered when no options are available. */
empty?: () => any
/** Content rendered below the option list. Stays pinned below the
* scrollable options. Receives the same shape as `#trigger`. */
footer?: (props: SelectSlotProps) => any
}
/**
* Item slot names: the three fixed regions of the row shell, plus any
* `#item-<name>` dispatched from an option's `slot` field.
*
* The index signature is deliberately narrowed to `` `item-${string}` `` —
* `Select` resolves `option.slot` to `` `item-${option.slot}` ``, so this is
* exactly the runtime behavior, and every fixed slot name stays typechecked
* instead of every typo compiling clean.
*/
interface SelectItemSlotsByName {
/** Content rendered before the standard option label. */
'item-prefix'?: (props: SelectItemSlotProps) => any
/** Content rendered for the standard option label area. */
'item-label'?: (props: SelectItemSlotProps) => any
/** Content rendered after the standard option label. */
'item-suffix'?: (props: SelectItemSlotProps) => any
/** Per-option dynamic slot, selected by the option's `slot` field. */
[slotName: `item-${string}`]:
| ((props: SelectItemSlotProps) => any)
| undefined
}
export interface SelectSlots extends SelectFixedSlots, SelectItemSlotsByName {}
/**
* The events this interface names for a wrapper to reuse. `update:modelValue`
* is not here: `defineModel` declares it, and the generated payload carries
* `undefined` because the model prop is optional. Declaring it again published
* one event twice with two payload types, so a wrapper that re-bound
* `@update:model-value` failed to compile. `ComboboxEmits` and
* `MultiSelectEmits` dropped their model events for the same reason. The
* runtime event is unchanged: the component emits a value or `null`.
*/
export interface SelectEmits {
/** Fired when the open state changes. */
'update:open': [value: boolean]
}Size of the select input.
Visual style of the select input.
Placeholder text displayed when no option is selected.
If true, disables the select input.
The currently selected value. `null` when nothing is selected.
Controls the visibility of the select menu.
Options to display in the dropdown.
Fallback empty-state copy rendered when no options are available.
Preferred popover side. Defaults to `'bottom'`. Setting `side`, `align`, or `offset` switches the menu from its default item-aligned placement (anchored over the trigger, macOS-style) to standard popper placement below/beside the trigger.
Preferred popover alignment. Defaults to `'start'`. See `side`.
Gap in px between trigger and content. Defaults to `4`. See `side`.
Teleport target for the popover content. Unset, an embedding host's target is used, else `body`.
Label rendered above (or beside, for binary controls) the input.
Helper text rendered below the input. Hidden when `error` is set. A `#description` slot is not: it renders beside the error, and is referenced alongside it.
Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. Takes a string, an array of strings, or an `Error` whose `messages` are rendered as stacked lines (with `Error.message` as the fallback). This is the same value `ErrorMessage.message` takes. An empty array and an empty string both mean no error.
Marks the field as required. Renders an asterisk next to the label, with `sr-only` text that announces it, and forwards `required` / `aria-required` to the underlying control where the control's role allows it. `data-required` is set either way.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
| Slot | Payload |
|---|---|
trigger | SelectSlotProps Fully custom trigger renderer. |
label | { required: boolean; } Overrides the rendered label content. Receives `{ required }`. |
description | — Overrides the rendered description content. |
prefix | SelectSlotProps Content rendered before the trigger value. Receives the same shape as `#trigger` and `#suffix` (`SelectSlotProps`). |
suffix | SelectSlotProps Content rendered after the trigger value. Providing this slot **replaces the default chevron** — render your own fallback when your slot content is conditional. |
item | SelectItemSlotProps Replaces the entire option row, including the row shell. A per-option `slot` (`#item-<name>`) takes precedence over this slot. |
empty | — Fallback content rendered when no options are available. |
footer | SelectSlotProps Content rendered below the option list. Stays pinned below the scrollable options. Receives the same shape as `#trigger`. |
item-prefix | SelectItemSlotProps Content rendered before the standard option label. |
item-label | SelectItemSlotProps Content rendered for the standard option label area. |
item-suffix | SelectItemSlotProps Content rendered after the standard option label. |
Fully custom trigger renderer.
Overrides the rendered label content. Receives `{ required }`.
Overrides the rendered description content.
Content rendered before the trigger value. Receives the same shape as `#trigger` and `#suffix` (`SelectSlotProps`).
Content rendered after the trigger value. Providing this slot **replaces the default chevron** — render your own fallback when your slot content is conditional.
Replaces the entire option row, including the row shell. A per-option `slot` (`#item-<name>`) takes precedence over this slot.
Fallback content rendered when no options are available.
Content rendered below the option list. Stays pinned below the scrollable options. Receives the same shape as `#trigger`.
Content rendered before the standard option label.
Content rendered for the standard option label area.
Content rendered after the standard option label.
| Event | Payload |
|---|---|
update:open | [value: boolean] Fired when the open state changes. |
update:modelValue | [value: SelectOptionValue | null | undefined] Fired when the model value changes. |
Fired when the open state changes.
Fired when the model value changes.
| Template ref | Type |
|---|---|
clear | () => void Clears the current selection. It leaves the search query alone. |
focus | (options?: FocusOptions) => void Moves focus to the component's interactive element. |
Clears the current selection. It leaves the search query alone.
Moves focus to the component's interactive element.