Select

Lets users select one option from a list. Ideal for forms, settings, or any interface where a single choice is required.

Example

Auto width
Matches the widest option by default, closer to a native select.
Value: No selection yet
Full width
Opt in with class="w-full" when you want the trigger to fill its container.
Value: strawberry-cheesecake
vue
<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.

vue
<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

Disabled label option
Common sorting pattern where a disabled first option keeps an empty string value without breaking the select.
Value: Empty string
vue
<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

Prefix and suffix
Use the default trigger shell when you only need light customization.
Custom trigger
Replace the trigger content entirely when you need richer layout.
vue
<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.

We'll pick a default for you when you don't choose.

vue
<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-suffix when you want to customize the standard option row.
  • Use v-model:open when you need to control the menu state.
  • By default, Select sizes itself to fit its option content. Set class="w-full" when you want a full-width trigger.
  • Select accepts flat options only. Empty and nullish options are omitted.

API Reference

Show types
typescript
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
= "sm"
"md" | "sm" | "lg" | "xl"

Size of the select input.

variant
= "subtle"
"subtle" | "outline" | "ghost"

Visual style of the select input.

placeholder
= "Select option"
string

Placeholder text displayed when no option is selected.

disabled
boolean

If true, disables the select input.

modelValue
SelectOptionValue

The currently selected value.

open
= false
boolean

Controls the visibility of the select menu.

options
= []
SelectOption[]

Options to display in the dropdown.

emptyText
= "No options"
string

Fallback empty-state copy rendered when no options are available.

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.

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.

update:modelValue
[value: SelectOptionValue | undefined]

Fired when the model value changes.

update:open
[value: boolean]

Fired when the open state changes.