Select
selectuiDropdown select primitive — single-select with optional groups, descriptions per item, headless popover mode (reka-ui), and zero-JS styled NativeSelect for lightweight/mobile use.
When to use which
Decision GuideSelect is the headless primitive for flat options. Advance Select is the searchable combobox. Cascade and Tree selectors are designed specifically for hierarchical and nested datasets.
| Component | Role | Use when |
|---|---|---|
| Selectcurrent Short, flat option lists (< 20 items) where typing to filter is unnecessary. | Primitive Dropdown | |
| AdvanceSelect Large datasets requiring fuzzy search, multi-select tag pills, or remote option filtering. | Searchable Combobox | |
| CascadeSelect Sequential multi-level drilldowns (e.g. Country > State > City, Category > Subcategory). | Hierarchical Picker | |
| TreeSelect Deeply nested taxonomy structures, folder trees, or parent-child permission nodes. | Tree Picker |
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/select.jsonnpx shadcn-vue@latest add @uipkge/selectInstalls to:app/components/ui/select/Examples
Schema
Type aliases from this item's source — use them to shape the data you pass in.
NativeSelectOptioninterface NativeSelectOption {
label: string
value: string | number
disabled?: boolean
}npm dependencies
Used by
Files installed (14)
app/components/ui/select/Select.vue0.4 kB
<script setup lang="ts"> import type { SelectRootEmits, SelectRootProps } from 'reka-ui' import { SelectRoot, useForwardPropsEmits } from 'reka-ui' const props = defineProps<SelectRootProps>() const emits = defineEmits<SelectRootEmits>() const forwarded = useForwardPropsEmits(props, emits) </script> <template> <SelectRoot v-slot="slotProps" data-uipkge data-slot="select" v-bind="forwarded"> <slot v-bind="slotProps" /> </SelectRoot> </template>app/components/ui/select/SelectContent.vue2.3 kB
<script setup lang="ts"> import type { SelectContentEmits, SelectContentProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits } from 'reka-ui' import { cn } from '@/lib/utils' import { SelectScrollDownButton, SelectScrollUpButton } from '.' defineOptions({ inheritAttrs: false, }) const props = withDefaults(defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(), { position: 'popper', }) const emits = defineEmits<SelectContentEmits>() const delegatedProps = reactiveOmit(props, 'class') const forwarded = useForwardPropsEmits(delegatedProps, emits) </script> <template> <SelectPortal> <SelectContent data-uipkge data-slot="select-content" v-bind="{ ...$attrs, ...forwarded }" :class=" cn( 'bg-popover text-popover-foreground motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded-md border shadow-md motion-safe:data-[state=closed]:duration-[var(--dur-exit)] motion-safe:data-[state=open]:duration-200', position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', props.class, ) " > <SelectScrollUpButton /> <SelectViewport :class=" cn( 'p-1', position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1', ) " > <slot /> </SelectViewport> <SelectScrollDownButton /> </SelectContent> </SelectPortal> </template>app/components/ui/select/SelectGroup.vue0.3 kB
<script setup lang="ts"> import type { SelectGroupProps } from 'reka-ui' import { SelectGroup } from 'reka-ui' const props = defineProps<SelectGroupProps>() </script> <template> <SelectGroup data-uipkge data-slot="select-group" v-bind="props"> <slot /> </SelectGroup> </template>app/components/ui/select/SelectItem.vue1.4 kB
<script setup lang="ts"> import type { SelectItemProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Check } from 'lucide-vue-next' import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const forwardedProps = useForwardProps(delegatedProps) </script> <template> <SelectItem data-uipkge data-slot="select-item" v-bind="forwardedProps" :class=" cn( 'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2', props.class, ) " > <span class="absolute right-2 flex size-3.5 items-center justify-center"> <SelectItemIndicator> <slot name="indicator-icon"> <Check class="size-4" aria-hidden="true" /> </slot> </SelectItemIndicator> </span> <SelectItemText> <slot /> </SelectItemText> </SelectItem> </template>app/components/ui/select/SelectItemText.vue0.3 kB
<script setup lang="ts"> import type { SelectItemTextProps } from 'reka-ui' import { SelectItemText } from 'reka-ui' const props = defineProps<SelectItemTextProps>() </script> <template> <SelectItemText data-uipkge data-slot="select-item-text" v-bind="props"> <slot /> </SelectItemText> </template>app/components/ui/select/SelectLabel.vue0.4 kB
<script setup lang="ts"> import type { SelectLabelProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { SelectLabel } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>() </script> <template> <SelectLabel data-uipkge data-slot="select-label" :class="cn('text-muted-foreground px-2 py-2 text-xs', props.class)"> <slot /> </SelectLabel> </template>app/components/ui/select/SelectScrollDownButton.vue0.8 kB
<script setup lang="ts"> import type { SelectScrollDownButtonProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ChevronDown } from 'lucide-vue-next' import { SelectScrollDownButton, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const forwardedProps = useForwardProps(delegatedProps) </script> <template> <SelectScrollDownButton data-uipkge data-slot="select-scroll-down-button" v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)" > <slot> <ChevronDown class="size-4" aria-hidden="true" /> </slot> </SelectScrollDownButton> </template>app/components/ui/select/SelectScrollUpButton.vue0.8 kB
<script setup lang="ts"> import type { SelectScrollUpButtonProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ChevronUp } from 'lucide-vue-next' import { SelectScrollUpButton, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') const forwardedProps = useForwardProps(delegatedProps) </script> <template> <SelectScrollUpButton data-uipkge data-slot="select-scroll-up-button" v-bind="forwardedProps" :class="cn('flex cursor-default items-center justify-center py-1', props.class)" > <slot> <ChevronUp class="size-4" aria-hidden="true" /> </slot> </SelectScrollUpButton> </template>app/components/ui/select/SelectSeparator.vue0.6 kB
<script setup lang="ts"> import type { SelectSeparatorProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { SelectSeparator } from 'reka-ui' import { cn } from '@/lib/utils' const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>() const delegatedProps = reactiveOmit(props, 'class') </script> <template> <SelectSeparator data-uipkge data-slot="select-separator" v-bind="delegatedProps" :class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)" /> </template>app/components/ui/select/SelectTrigger.vue2.2 kB
<script setup lang="ts"> import type { SelectTriggerProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { ChevronDown, Loader } from 'lucide-vue-next' import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' const props = withDefaults( defineProps< SelectTriggerProps & { class?: HTMLAttributes['class'] size?: 'sm' | 'default' | 'lg' state?: 'default' | 'error' | 'success' loading?: boolean } >(), { size: 'default', state: 'default', loading: false }, ) const delegatedProps = reactiveOmit(props, 'class', 'size', 'state', 'loading') const forwardedProps = useForwardProps(delegatedProps) const sizeClasses = { sm: 'h-8 text-sm px-2.5 py-1.5', default: 'h-9 text-sm px-3 py-2', lg: 'h-11 text-base px-4 py-2.5', } const stateClasses = { default: 'border-input dark:hover:bg-input/50', error: 'border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', success: 'border-success focus-visible:border-success', } </script> <template> <SelectTrigger data-uipkge data-slot="select-trigger" :data-size="size" :data-state-value="state" v-bind="forwardedProps" :disabled="disabled || loading" :aria-busy="loading" :class=" cn( 'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex w-full items-center justify-between gap-2 rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', sizeClasses[size], stateClasses[state], props.class, ) " > <slot /> <SelectIcon v-if="!loading" as-child> <ChevronDown class="size-4 opacity-50" aria-hidden="true" /> </SelectIcon> <Loader v-else class="size-4 animate-spin opacity-50" aria-hidden="true" /> </SelectTrigger> </template>app/components/ui/select/SelectValue.vue0.3 kB
<script setup lang="ts"> import type { SelectValueProps } from 'reka-ui' import { SelectValue } from 'reka-ui' const props = defineProps<SelectValueProps>() </script> <template> <SelectValue data-uipkge data-slot="select-value" v-bind="props"> <slot /> </SelectValue> </template>app/components/ui/select/NativeSelect.vue2.7 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, useAttrs } from 'vue' import { useVModel } from '@vueuse/core' import { ChevronDown } from 'lucide-vue-next' import { cn } from '@/lib/utils' defineOptions({ inheritAttrs: false, }) export interface NativeSelectOption { label: string value: string | number disabled?: boolean } interface Props { modelValue?: string | number defaultValue?: string | number options?: (NativeSelectOption | string)[] size?: 'sm' | 'md' | 'lg' disabled?: boolean class?: HTMLAttributes['class'] selectClass?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { size: 'md', }) const emits = defineEmits<{ (e: 'update:modelValue', val: string | number): void }>() const attrs = useAttrs() const modelValue = useVModel(props, 'modelValue', emits, { passive: true, defaultValue: props.defaultValue, }) const sizeClasses: Record<string, string> = { sm: 'h-8 text-xs pl-2.5 pr-8', md: 'h-9 text-sm pl-3 pr-9', lg: 'h-11 text-base pl-4 pr-10', } const iconSizes: Record<string, string> = { sm: 'size-3.5 right-2.5', md: 'size-4 right-3', lg: 'size-5 right-3.5', } const normalizedOptions = computed(() => { if (!props.options) return [] return props.options.map((opt) => { if (typeof opt === 'string') { return { label: opt, value: opt, disabled: false } } return opt }) }) </script> <template> <div data-uipkge data-slot="native-select-wrapper" :class="cn('relative inline-flex w-full items-center', props.class)" > <select data-slot="native-select" :disabled="disabled" :value="modelValue" :class=" cn( 'border-input bg-background w-full appearance-none rounded-md border shadow-xs transition-[color,box-shadow]', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none', 'disabled:bg-muted/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50', sizeClasses[size], props.selectClass, ) " v-bind="attrs" @change="modelValue = ($event.target as HTMLSelectElement).value" > <template v-if="normalizedOptions.length > 0"> <option v-for="opt in normalizedOptions" :key="String(opt.value)" :value="opt.value" :disabled="opt.disabled"> {{ opt.label }} </option> </template> <slot v-else /> </select> <ChevronDown data-slot="native-select-icon" aria-hidden="true" :class=" cn( 'text-muted-foreground pointer-events-none absolute transition-opacity', disabled && 'opacity-50', iconSizes[size], ) " /> </div> </template>app/components/ui/select/index.ts0.8 kB
export { default as Select } from './Select.vue' export { default as SelectContent } from './SelectContent.vue' export { default as SelectGroup } from './SelectGroup.vue' export { default as SelectItem } from './SelectItem.vue' export { default as SelectItemText } from './SelectItemText.vue' export { default as SelectLabel } from './SelectLabel.vue' export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' export { default as SelectSeparator } from './SelectSeparator.vue' export { default as SelectTrigger } from './SelectTrigger.vue' export { default as SelectValue } from './SelectValue.vue' export { default as NativeSelect, type NativeSelectOption } from './NativeSelect.vue' export { type SelectOption, readKey } from './option-types'app/components/ui/select/option-types.ts0.7 kB
/** * Shared option shape for `AdvanceSelect` and any future options-driven select. * Components accept this canonical shape OR any object shape if `value-key` / * `label-key` are provided. */ export interface SelectOption<V = string> { label: string value: V disabled?: boolean /** Items sharing this key render under one heading. */ group?: string } /** * Resolve `option[key]` with a default fallback. Used by AdvanceSelect so every * accessor obeys `value-key` / `label-key`. */ export function readKey<T>(option: T, key: string, fallback?: unknown): unknown { if (option == null || typeof option !== 'object') return fallback const v = (option as Record<string, unknown>)[key] return v === undefined ? fallback : v }
Raw manifest:https://uipkge.dev/r/vue/select.json