Select
Lets users select one option from a list. Ideal for forms, settings, or any interface where a single choice is required.
Example
<script setup lang="ts">
import { ref } from 'vue'
import { Select } from 'frappe-ui'
const autoWidthValue = ref('')
const fullWidthValue = ref('strawberry-cheesecake')
const options = [
{
label: 'Matcha Tiramisu',
value: 'matcha-tiramisu',
},
{
label: 'Strawberry Cheesecake',
value: 'strawberry-cheesecake',
},
{
label: 'Chocolate Lava Cake',
value: 'chocolate-lava-cake',
},
{
label: 'Mango Sticky Rice',
value: 'mango-sticky-rice',
disabled: true,
},
{
label: 'Pistachio Baklava',
value: 'pistachio-baklava',
},
{
label: 'Ube Ice Cream',
value: 'ube-ice-cream',
},
{
label: 'Salted Caramel Tart',
value: 'salted-caramel-tart',
},
]
</script>
<template>
<div class="w-full max-w-4xl">
<div
class="grid divide-y divide-outline-gray-2 md:grid-cols-2 md:divide-x md:divide-y-0"
>
<div class="pb-8 md:pr-8">
<div class="text-sm font-medium text-ink-gray-7">Auto width</div>
<div class="mt-1 text-p-sm text-ink-gray-5">
Matches the widest option by default, closer to a native select.
</div>
<div class="mt-4">
<Select
v-model="autoWidthValue"
:options="options"
variant="outline"
/>
</div>
<div class="mt-4 text-sm text-ink-gray-5">
Value:
<span class="text-ink-gray-8">
{{ autoWidthValue || 'No selection yet' }}
</span>
</div>
</div>
<div class="pb-8 md:pl-8">
<div class="text-sm font-medium text-ink-gray-7">Full width</div>
<div class="mt-1 text-p-sm text-ink-gray-5">
Opt in with
<code class="text-sm text-ink-gray-7">class="w-full"</code>
when you want the trigger to fill its container.
</div>
<div class="mt-4 w-[320px] max-w-full">
<Select
v-model="fullWidthValue"
:options="options"
variant="outline"
class="w-full"
/>
</div>
<div class="mt-4 text-sm text-ink-gray-5">
Value:
<span class="text-ink-gray-8">{{ fullWidthValue }}</span>
</div>
</div>
</div>
</div>
</template>Custom Option Layout
Use #item-prefix and #item-label to tailor the standard row — for example, an avatar plus a two-line label with a secondary description. #prefix on the trigger reuses the selected option's accessory.
<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>States
<script setup lang="ts">
import { ref } from 'vue'
import { Select } from 'frappe-ui'
const sortOrder = ref('')
const sortOptions = [
{
label: 'Sort by',
value: '',
disabled: true,
},
{
label: 'Newest first',
value: 'newest',
},
{
label: 'Oldest first',
value: 'oldest',
},
{
label: 'Most popular',
value: 'popular',
},
]
</script>
<template>
<div class="w-full max-w-md py-4">
<div class="text-sm font-medium text-ink-gray-7">Disabled label option</div>
<div class="mt-1 text-p-sm text-ink-gray-5">
Common sorting pattern where a disabled first option keeps an empty string
value without breaking the select.
</div>
<div class="mt-4">
<Select v-model="sortOrder" :options="sortOptions" variant="outline" />
</div>
<div class="mt-4 text-sm text-ink-gray-5">
Value:
<span class="text-ink-gray-8">
{{ sortOrder === '' ? 'Empty string' : sortOrder }}
</span>
</div>
</div>
</template>Trigger Slots
<script setup lang="ts">
import { ref } from 'vue'
import { Select } from 'frappe-ui'
const assignee = ref('faris')
const reviewer = ref('mariam')
const options = [
{
label: 'Faris',
value: 'faris',
},
{
label: 'Aakvatech',
value: 'aakvatech',
},
{
label: 'Hadi',
value: 'hadi',
},
{
label: 'Mariam',
value: 'mariam',
},
{
label: 'Suhail',
value: 'suhail',
},
]
</script>
<template>
<div
class="grid w-full max-w-4xl divide-y divide-outline-gray-2 md:grid-cols-2 md:divide-x md:divide-y-0"
>
<div class="py-4 md:pr-8">
<div class="text-sm font-medium text-ink-gray-7">Prefix and suffix</div>
<div class="mt-1 text-p-sm text-ink-gray-5">
Use the default trigger shell when you only need light customization.
</div>
<div class="mt-4">
<Select v-model="assignee" :options="options" variant="outline">
<template #prefix>
<span class="lucide-user size-4 text-ink-gray-6" />
</template>
<template #suffix>
<div class="ml-auto flex items-center gap-2 text-ink-gray-5">
<span
class="rounded bg-surface-gray-2 px-1.5 py-0.5 text-sm text-ink-gray-6"
>
5
</span>
<span class="lucide-chevron-down size-4 text-ink-gray-6" />
</div>
</template>
</Select>
</div>
</div>
<div class="py-4 md:pl-8">
<div class="text-sm font-medium text-ink-gray-7">Custom trigger</div>
<div class="mt-1 text-p-sm text-ink-gray-5">
Replace the trigger content entirely when you need richer layout.
</div>
<div class="mt-4 w-[320px] max-w-full">
<Select
v-model="reviewer"
:options="options"
variant="outline"
class="w-full"
>
<template #trigger="{ displayValue, open }">
<div class="flex w-full items-center gap-3">
<div
class="flex size-7 shrink-0 items-center justify-center rounded-full bg-surface-gray-2"
>
<span class="lucide-users size-4 text-ink-gray-6" />
</div>
<div class="min-w-0 flex-1 py-1.5">
<div class="truncate">
{{ displayValue || 'Choose reviewer' }}
</div>
<div class="truncate text-p-sm text-ink-gray-5">
Design review queue
</div>
</div>
<span
:class="[
'lucide-chevron-down size-4 shrink-0 text-ink-gray-4 transition-transform duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]',
open ? 'rotate-180' : '',
]"
/>
</div>
</template>
</Select>
</div>
</div>
</div>
</template>Label, Description, Error
Select 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.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Checkbox, Select } from 'frappe-ui'
const value = ref('')
const required = ref(true)
const showError = ref(false)
const error = computed(() =>
showError.value ? 'Please choose a fruit.' : '',
)
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
]
</script>
<template>
<div class="flex gap-8 items-start">
<Select
v-model="value"
:options="options"
label="Favourite fruit"
description="We'll pick a default for you when you don't choose."
:error="error"
:required="required"
placeholder="Pick one"
class="w-72"
/>
<div
class="flex flex-col gap-2 items-start border-l border-outline-gray-2 pl-6"
>
<Checkbox v-model="required" label="required" />
<Checkbox v-model="showError" label="show error" />
</div>
</div>
</template>Notes
- Prefer
#item-prefix,#item-label, and#item-suffixwhen you want to customize the standard option row. - Use
v-model:openwhen you need to control the menu state. - By default,
Selectsizes itself to fit its option content. Setclass="w-full"when you want a full-width trigger. Selectaccepts flat options only. Empty and nullish options are omitted.
API Reference
Show types
import type { Component } from 'vue'
import type { InputLabelingProps } from '../../composables/useInputLabeling'
export type SelectOptionValue = string | number | bigint | Record<string, any>
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?: 'sm' | 'md' | 'lg' | 'xl'
/** Visual style of the select input. */
variant?: 'subtle' | 'outline' | 'ghost'
/** Placeholder text displayed when no option is selected. */
placeholder?: string
/** If true, disables the select input. */
disabled?: boolean
/** The currently selected value. */
modelValue?: SelectOptionValue
/** 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
}
export interface SelectTriggerSlotProps {
/** Whether the select menu is currently open. */
open: boolean
/** Whether the trigger is disabled. */
disabled: boolean
/** Currently selected option, if any. */
selectedOption: SelectNormalizedOption | null
/** Plain-text label shown in the trigger. */
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 SelectSlotProps = SelectTriggerSlotProps
export type SelectPrefixSlotProps = SelectSlotProps
export type SelectSuffixSlotProps = SelectSlotProps
export interface SelectItemSlotProps {
/** Item currently being rendered. */
item: SelectNormalizedOption
/**
* @deprecated Use `item`. Retained as a silent alias through v1.x for
* back-compat with the pre-v1 `{ option }` slot-prop shape.
*/
option: SelectNormalizedOption
}
export interface SelectSlots {
/** 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
/**
* Shared renderer for option labels.
* @deprecated use `#item-label` for per-row label customization. `#option` remains as a back-compat alias through v1.x.
*/
option?: (props: SelectItemSlotProps) => any
/** 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
/** Fallback content rendered when no options are available. */
empty?: () => any
/** Content rendered below the option list. */
footer?: () => any
[slotName: string]: ((props: any) => any) | undefined
}
export interface SelectEmits {
/** Fired when the selected value changes. */
'update:modelValue': [value: SelectOptionValue | undefined]
/** Fired when the open state changes. */
'update:open': [value: boolean]
}
export interface SelectExposed {}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.
Controls the visibility of the select menu.
Options to display in the dropdown.
Fallback empty-state copy rendered when no options are available.
Label rendered above (or beside, for binary controls) the input.
Helper text rendered below the input. Hidden when `error` is set.
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).
Marks the field as required. Renders an asterisk next to the label and forwards `required` / `aria-required` to the underlying control.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
| Slot | Payload |
|---|---|
trigger | SelectTriggerSlotProps Fully custom trigger renderer. |
label | { required: boolean; } Overrides the rendered label content. Receives `{ required }`. |
description | — Overrides the rendered description content. |
prefix | SelectTriggerSlotProps Content rendered before the trigger value. Receives the same shape as `#trigger` and `#suffix` (`SelectSlotProps`). |
suffix | SelectTriggerSlotProps Content rendered after the trigger value. Providing this slot **replaces the default chevron** — render your own fallback when your slot content is conditional. |
option | Deprecated — use `#item-label` for per-row label customization. `#option` remains as a back-compat alias through v1.x. |
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. |
empty | — Fallback content rendered when no options are available. |
footer | — Content rendered below the option list. |
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.
Deprecated — use `#item-label` for per-row label customization. `#option` remains as a back-compat alias through v1.x.
Content rendered before the standard option label.
Content rendered for the standard option label area.
Content rendered after the standard option label.
Fallback content rendered when no options are available.
Content rendered below the option list.
| Event | Payload |
|---|---|
update:modelValue | [value: SelectOptionValue | undefined] Fired when the model value changes. |
update:open | [value: boolean] Fired when the open state changes. |
Fired when the model value changes.
Fired when the open state changes.