Select
Vue controlDropdown select primitive — single-select, with optional groups, descriptions per item, and a search input via the AdvanceSelect variant. Built on reka-ui.
Also available for React ->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.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/select
Examples
Dependencies
Used by
Files (13)
-
app/components/ui/select/Select.vue 0.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.vue 2.1 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=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-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md', 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.vue 0.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.vue 1.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.vue 0.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.vue 0.5 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-1.5 text-xs', props.class)" > <slot /> </SelectLabel> </template> -
app/components/ui/select/SelectScrollDownButton.vue 0.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.vue 0.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.vue 0.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.vue 2.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-[var(--success)] focus-visible:border-[var(--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" /> </SelectTrigger> </template> -
app/components/ui/select/SelectValue.vue 0.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/index.ts 0.7 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 { type SelectOption, readKey } from './option-types' -
app/components/ui/select/option-types.ts 0.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