Select

Displays a list of options for the user to pick from—triggered by a button.

Examples

Basic

NameDefaultTypeDescription
items-arraySet the select items.
dirltrstringThe direction of the select. Values: ltr, rtl
placeholder-stringThe content that will be rendered inside the SelectValue when no value or defaultValue is set.
label-stringSet the select items label.
defaultOpen-booleanThe open state of the select when it is initially rendered. Use when you do not need to control its open state.
defaultValue-stringThe value of the select when initially rendered. Use when you do not need to control the state of the Select
open-booleanThe controlled open state of the Select. Can be bind as v-model:open.
modelValue-stringThe controlled value of the Select. Can be bind as v-model.
status-stringSet the status of the select. Values: info, success, warning, error
Preview
Code

Multiple

PropDefaultTypeDescription
multiplefalsebooleanEnable multiple selection mode.
Preview
Code
Selected:

Disabled

PropDefaultTypeDescription
disabledfalsebooleanDisable the select component.
Preview
Code

Group

PropDefaultTypeDescription
group-booleanEnable support for group items.
Preview
Code

Objects

Control the attribute value to be displayed in the select and the item.

PropDefaultTypeDescription
valueKey-stringThe key name to be displayed in the selected value.
itemKey-stringThe key name to be displayed in the item.
Preview
Code
Output:

Form Field

The NSelect component can be easily embedded within the NFormField component.

Preview
Code

Select a contributor from the Vue community

Variant and Color

PropDefaultTypeDescription
selectsoft-white{variant}-{color}Change the color of the select.
select-itemgray{color}Change the color of the select item.
Preview
Code

Size

Adjust the select size without limits. Use breakpoints (e.g., sm:sm, xs:lg) for responsive sizes or states (e.g., hover:lg, focus:3xl) for state-based sizes.

PropDefaultTypeDescription
sizesmstringAdjusts the overall size of the select component.
_selectItem.sizesmstringCustomizes the size of each item within the select dropdown.
_selectTrigger.sizesmstringModifies the size of the select trigger element.
Preview
Code

Slots

NamePropsDescription
rootvalueOverrides all sub-components.
trigger-wrappervalueOverride the default trigger button.
triggervalueThe trigger slot.
valuevalueThe value slot.
contentitemsThe content slot.
labellabelThe label slot.
itemitemThe item slot.
groupitemsThe group slot.
Preview
Code

Presets

shortcuts/select.ts
import type { Theme } from '@unocss/preset-mini'
import type { RuleContext } from 'unocss'
import { parseColor } from '@unocss/preset-mini'

type SelectPrefix = 'select'

export const staticSelect: Record<`${SelectPrefix}-${string}` | SelectPrefix, string> = {
  // configurations
  'select': '',
  'select-default-variant': 'btn-solid-white',
  'select-disabled': 'n-disabled',
  'select-scroll': 'flex cursor-default items-center justify-center py-1',
  'select-trigger-info-icon': 'i-info',
  'select-trigger-error-icon': 'i-error',
  'select-trigger-success-icon': 'i-success',
  'select-trigger-warning-icon': 'i-warning',

  // components
  'select-trigger': 'px-0.8571428571428571em w-full [&>span]:truncate',
  'select-trigger-trailing-icon': 'i-lucide-chevron-down',
  'select-trigger-trailing': 'size-1.4285714285714286em data-[status=error]:text-error data-[status=success]:text-success data-[status=warning]:text-warning data-[status=info]:text-info data-[status=default]:(n-disabled size-1.1428571428571428em)',
  'select-trigger-leading': 'size-1.1428571428571428em',

  'select-value': 'text-1em data-[status=error]:text-error-active data-[status=success]:text-success-active data-[status=warning]:text-warning-active data-[status=info]:text-info-active data-[placeholder]:n-disabled',

  'select-content': 'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border border-base bg-popover text-popover shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
  'select-content-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',

  'select-group': 'p-1 w-full',

  'select-separator': '-mx-1 my-1 h-px bg-muted',

  'select-item': 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-1em outline-none data-[disabled]:pointer-events-none data-[disabled]:n-disabled',

  'select-item-indicator': 'absolute left-2 size-1.1428571428571428em flex items-center justify-center',
  'select-item-indicator-icon': 'i-check',

  'select-viewport': 'p-1',
  'select-viewport-popper': 'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]',

  'select-scroll-up-button': 'select-scroll',
  'select-scroll-down-button': 'select-scroll',
  'select-scroll-up-button-icon': 'i-lucide-chevron-up',
  'select-scroll-down-button-icon': 'i-lucide-chevron-down',

  'select-label': 'py-1.5 pl-8 pr-2 text-1em font-semibold',

  // ⚠️ for overriding purposes only
  'select-item-selectItem': '',
}

export const dynamicSelect = [
  [/^select-([^-]+)-([^-]+)$/, ([, v = 'solid', c = 'gray'], { theme }: RuleContext<Theme>) => {
    const parsedColor = parseColor(c, theme)
    if ((parsedColor?.cssColor?.type === 'rgb' || parsedColor?.cssColor?.type === 'rgba') && parsedColor.cssColor.components)
      return `btn-${v}-${c}`
    return undefined
  }],

  [/^select-item(-(\S+))?$/, ([, , c = 'gray'], { theme }: RuleContext<Theme>) => {
    const parsedColor = parseColor(c || 'gray', theme)
    if ((parsedColor?.cssColor?.type === 'rgb' || parsedColor?.cssColor?.type === 'rgba') && parsedColor.cssColor.components)
      return `focus:bg-${c || 'gray'}-100 focus:text-${c || 'gray'}-800 dark:focus:bg-${c || 'gray'}-800 dark:focus:text-${c || 'gray'}-100`
    return undefined
  }],
]

export const select = [
  ...dynamicSelect,
  staticSelect,
]

Props

types/select.ts
import type { AcceptableValue, SelectContentProps, SelectGroupProps, SelectItemIndicatorProps, SelectItemProps, SelectItemTextProps, SelectLabelProps, SelectRootProps, SelectScrollDownButtonProps, SelectScrollUpButtonProps, SelectSeparatorProps, SelectTriggerProps, SelectValueProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'

interface BaseExtensions {
  class?: HTMLAttributes['class']
  size?: HTMLAttributes['class']
}

type TriggerExtensions = SelectTriggerProps & Omit<NButtonProps, 'una'> & BaseExtensions
type ValueExtensions = SelectValueProps & BaseExtensions
type ScrollDownButtonExtensions = SelectScrollDownButtonProps & BaseExtensions
type ScrollUpButtonExtensions = SelectScrollUpButtonProps & BaseExtensions
type ContentExtensions = SelectContentProps & BaseExtensions
type ItemExtensions = SelectItemProps & BaseExtensions
type ItemTextExtensions = SelectItemTextProps & BaseExtensions
type GroupExtensions = SelectGroupProps & BaseExtensions
type LabelExtensions = SelectLabelProps & BaseExtensions
type SeparatorExtensions = SelectSeparatorProps & BaseExtensions
type SelectExtensions = SelectRootProps
  & BaseExtensions
  & Pick<NSelectValueProps, 'placeholder'>
  & Pick<NSelectItemProps, 'selectItem'>
  & Pick<NSelectTriggerProps, 'status' | 'select' | 'id'>

export interface SelectGroup<T extends AcceptableValue> {
  label?: string
  items: T[]
  _selectLabel?: Partial<NSelectLabelProps>
  _selectItem?: Partial<NSelectItemProps>
}

export interface NSelectProps<T extends AcceptableValue> extends SelectExtensions {
  /**
   * s
   * The items to display in the select.
   */
  items: T[] | SelectGroup<T>[]
  /**
   * The key name to use to display in the select items.
   */
  itemKey?: keyof T
  /**
   * The key name to use to display in the selected value.
   */
  valueKey?: keyof T
  /**
   * The label to display above the select items.
   */
  label?: string
  /**
   * Allows for multiple groups within the select.
   */
  group?: boolean
  /**
   * Sub-component configurations
   */
  _selectScrollUpButton?: Partial<NSelectScrollUpButtonProps>
  _selectItemText?: Partial<NSelectItemTextProps>
  _selectScrollDownButton?: Partial<NSelectScrollDownButtonProps>
  _selectGroup?: Partial<NSelectGroupProps>
  _selectSeparator?: Partial<NSelectSeparator>
  _selectContent?: Partial<NSelectContentProps>
  _selectValue?: Partial<NSelectValueProps>
  _selectTrigger?: Partial<NSelectTriggerProps>
  _selectItem?: Partial<NSelectItemProps>
  _selectLabel?: Partial<NSelectLabelProps>

  una?: NSelectUnaProps
}

export interface NSelectTriggerProps extends TriggerExtensions {
  /**
   * The unique id of the select trigger to be used for the form field.
   */
  id?: string
  /**
   * Allows you to add `UnaUI` button preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/select.ts
   * @example
   * select="solid-green"
   */
  select?: string
  /**
   * The status of the select input.
   */
  status?: 'info' | 'success' | 'warning' | 'error'
  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/select.ts
   */
  una?: Pick<NSelectUnaProps, 'selectTrigger' | 'selectTriggerTrailing' | 'selectTriggerTrailingIcon' | 'selectTriggerLeading' | 'selectTriggerInfoIcon' | 'selectTriggerSuccessIcon' | 'selectTriggerWarningIcon' | 'selectTriggerErrorIcon'> & NButtonProps['una']
}

export interface NSelectValueProps extends ValueExtensions {
  una?: Pick<NSelectUnaProps, 'selectValue'>
}

export interface NSelectScrollDownButtonProps extends ScrollDownButtonExtensions {
  una?: Pick<NSelectUnaProps, 'selectScrollDownButton' | 'selectScrollDownButtonIcon'>
}

export interface NSelectScrollUpButtonProps extends ScrollUpButtonExtensions {
  una?: Pick<NSelectUnaProps, 'selectScrollUpButton' | 'selectScrollUpButtonIcon'>
}

export interface NSelectContentProps extends ContentExtensions {
  _selectScrollDownButton?: NSelectScrollDownButtonProps
  _selectScrollUpButton?: NSelectScrollUpButtonProps
  _selectSeparator?: NSelectSeparator
  una?: Pick<NSelectUnaProps, 'selectContent'>
}

export interface NSelectItemIndicatorProps extends SelectItemIndicatorProps {
  icon?: HTMLAttributes['class']
  class?: HTMLAttributes['class']
  una?: Pick<NSelectUnaProps, 'selectItemIndicator' | 'selectItemIndicatorIcon'>
}

export interface NSelectItemProps extends ItemExtensions {
  selectItem?: HTMLAttributes['class']
  isSelected?: boolean
  _selectItemText?: NSelectItemTextProps
  _selectItemIndicator?: NSelectItemIndicatorProps
  una?: Pick<NSelectUnaProps, 'selectItem' | 'selectItemIndicator'>
}

export interface NSelectItemTextProps extends ItemTextExtensions {
  una?: Pick<NSelectUnaProps, 'selectItemText'>
}

export interface NSelectGroupProps extends GroupExtensions {
  una?: Pick<NSelectUnaProps, 'selectGroup'>
}

export interface NSelectLabelProps extends LabelExtensions {
  una?: Pick<NSelectUnaProps, 'selectLabel'>
}

export interface NSelectSeparator extends SeparatorExtensions {
  una?: Pick<NSelectUnaProps, 'selectSeparator'>
}

export interface NSelectUnaProps {
  select?: HTMLAttributes['class']
  selectTrigger?: HTMLAttributes['class']
  selectTriggerTrailing?: HTMLAttributes['class']
  selectTriggerTrailingIcon?: HTMLAttributes['class']
  selectTriggerLeading?: HTMLAttributes['class']
  selectTriggerInfoIcon?: HTMLAttributes['class']
  selectTriggerSuccessIcon?: HTMLAttributes['class']
  selectTriggerWarningIcon?: HTMLAttributes['class']
  selectTriggerErrorIcon?: HTMLAttributes['class']
  selectValue?: HTMLAttributes['class']
  selectContent?: HTMLAttributes['class']
  selectItem?: HTMLAttributes['class']
  selectItemText?: HTMLAttributes['class']
  selectItemIndicator?: HTMLAttributes['class']
  selectItemIndicatorIcon?: HTMLAttributes['class']
  selectGroup?: HTMLAttributes['class']
  selectLabel?: HTMLAttributes['class']
  selectSeparator?: HTMLAttributes['class']
  selectScrollDownButton?: HTMLAttributes['class']
  selectScrollDownButtonIcon?: HTMLAttributes['class']
  selectScrollUpButton?: HTMLAttributes['class']
  selectScrollUpButtonIcon?: HTMLAttributes['class']
}

Components

Select.vue
SelectContent.vue
SelectGroup.vue
SelectItem.vue
SelectItemText.vue
SelectLabel.vue
lectScrollDownButton.vue
SelectScrollUpButton.vue
SelectSeperator.vue
SelectTrigger.vue
SelectValue.vue
<script lang="ts">
import type { AcceptableValue, SelectRootEmits } from 'reka-ui'
import type { NSelectProps, SelectGroup as SelectGroupType } from '../../../types'
</script>

<script setup lang="ts" generic="T extends AcceptableValue">
import { SelectRoot, useForwardPropsEmits } from 'reka-ui'
import { cn, isEqualObject } from '../../../utils'
import SelectContent from './SelectContent.vue'
import SelectGroup from './SelectGroup.vue'
import SelectItem from './SelectItem.vue'
import SelectLabel from './SelectLabel.vue'
import SelectSeparator from './SelectSeparator.vue'
import SelectTrigger from './SelectTrigger.vue'
import SelectValue from './SelectValue.vue'

const props = withDefaults(defineProps<NSelectProps<T>>(), {
  size: 'sm',
})

const emits = defineEmits<SelectRootEmits>()

const forwarded = useForwardPropsEmits(props, emits)

function formatSelectedValue(value: unknown) {
  if (!value || (Array.isArray(value) && value.length === 0))
    return null

  if (props.multiple && Array.isArray(value)) {
    return value.map((val) => {
      if (props.valueKey && typeof val === 'object') {
        return (val as Record<string, any>)[props.valueKey as string]
      }
      return val
    }).join(', ')
  }

  if (props.valueKey && typeof value === 'object') {
    return (value as Record<string, any>)[props.valueKey as string]
  }

  return value
}

function isItemSelected(item: unknown, modelValue: unknown) {
  if (!modelValue)
    return false

  if (props.multiple && Array.isArray(modelValue)) {
    return modelValue.some((val) => {
      const valObj = typeof val === 'object' && val ? val : { value: val }
      const itemObj = typeof item === 'object' && item ? item : { value: item }
      return isEqualObject(valObj, itemObj)
    })
  }

  const modelObj = typeof modelValue === 'object' && modelValue ? modelValue : { value: modelValue }
  const itemObj = typeof item === 'object' && item ? item : { value: item }
  return isEqualObject(modelObj, itemObj)
}
</script>

<template>
  <SelectRoot
    v-slot="{ modelValue, open }"
    :class="cn(
      props.una?.select,
      props.class,
    )"
    v-bind="forwarded"
  >
    <slot name="root" :model-value :open>
      <slot name="trigger-wrapper">
        <SelectTrigger
          :id
          :size
          :status
          :select
          v-bind="props._selectTrigger"
          :una
        >
          <slot name="trigger" :model-value :open="open">
            <SelectValue
              :placeholder="props.placeholder"
              v-bind="props._selectValue"
              :aria-label="formatSelectedValue(modelValue)"
              :data-status="status"
              :una
            >
              <slot name="value" :model-value :open>
                {{ formatSelectedValue(modelValue) || props.placeholder }}
              </slot>
            </SelectValue>
          </slot>
        </SelectTrigger>
      </slot>

      <SelectContent
        :size
        v-bind="{
          ..._selectContent,
          _selectScrollDownButton,
          _selectScrollUpButton,
        }"
        :una
      >
        <slot name="content" :items="items">
          <template v-if="!group">
            <SelectLabel
              v-if="label"
              v-bind="_selectLabel"
              :una
            >
              <slot name="label" :label>
                {{ label }}
              </slot>
            </SelectLabel>

            <template
              v-for="item in items"
              :key="item"
            >
              <SelectItem
                :value="item"
                :size
                :select-item
                v-bind="props._selectItem"
                :is-selected="isItemSelected(item, modelValue)"
                :una
              >
                <slot name="item" :item="item">
                  {{ props.itemKey && item ? (item as any)[props.itemKey] : item }}
                </slot>
              </SelectItem>
            </template>
          </template>

          <template v-if="group">
            <SelectGroup
              v-for="(group, i) in items as SelectGroupType<T>[]"
              :key="i"
              v-bind="props._selectGroup"
              :una
            >
              <SelectSeparator
                v-if="i > 0"
                v-bind="props._selectSeparator"
                :una
              />

              <slot name="group" :items="group">
                <SelectLabel
                  v-if="group.label"
                  :size
                  v-bind="{ ...props._selectLabel, ...group._selectLabel }"
                  :una
                >
                  <slot name="label" :label="group.label">
                    {{ group.label }}
                  </slot>
                </SelectLabel>

                <template
                  v-for="item in group.items"
                  :key="item"
                >
                  <SelectItem
                    :value="item"
                    :size
                    :select-item
                    v-bind="{ ..._selectItem, ...group._selectItem }"
                    :is-selected="isItemSelected(item, modelValue)"
                    :una
                  >
                    <slot name="item" :item="item">
                      {{ props.itemKey ? (item as any)[props.itemKey] : item }}
                    </slot>
                  </SelectItem>
                </template>
              </slot>
            </SelectGroup>
          </template>
          <slot />
        </slot>
      </SelectContent>
    </slot>
  </SelectRoot>
</template>