UIPackage

Select

Vue control
Edit on GitHub

Dropdown 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

$ npx 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