Select

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

Examples

Basic

NameDefaultTypeDescription
items-arraySet the select items.
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
triggervalueThe trigger slot.
valuevalueThe value slot.
contentitemsThe content slot.
labellabelThe label slot.
itemitemThe item slot.
groupitemsThe group slot.
Preview
Code

Presets

shortcuts/select.ts
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-root': '',
  'select-trigger': 'w-full [&>span]:truncate',
  'select-trigger-trailing-icon': 'i-lucide-chevron-down n-disabled',
  'select-trigger-trailing': 'ml-auto data-[status]:text-1.2em data-[status=error]:text-error data-[status=success]:text-success data-[status=warning]:text-warning data-[status=info]:text-info',
  'select-trigger-leading': '',

  'select-value': 'h-1.5em data-[status=error]:text-error data-[status=success]:text-success data-[status=warning]:text-warning data-[status=info]:text-info 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': 'select-item-gray 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 h-0.75em w-0.75em 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']) => `btn-${v}-${c}`],

  [/^select-item(-(\S+))?$/, ([, , c = 'gray']) => `focus:bg-${c}-100 focus:text-${c}-800 dark:focus:bg-${c}-800 dark:focus:text-${c}-100`],
]

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 RootExtensions = SelectRootProps & BaseExtensions
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 = NSelectRootProps
  & 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>
}

export interface NSelectRootProps extends RootExtensions {
  una?: {
    selectRoot?: HTMLAttributes['class']
  }
}

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?: {
    selectTrigger?: HTMLAttributes['class']
    selectTriggerTrailing?: HTMLAttributes['class']
    selectTriggerTrailingIcon?: HTMLAttributes['class']
    selectTriggerLeading?: HTMLAttributes['class']
    selectTriggerInfoIcon?: HTMLAttributes['class']
    selectTriggerSuccessIcon?: HTMLAttributes['class']
    selectTriggerWarningIcon?: HTMLAttributes['class']
    selectTriggerErrorIcon?: HTMLAttributes['class']
  } & NButtonProps['una']
}

export interface NSelectValueProps extends ValueExtensions {
  una?: {
    selectValue?: HTMLAttributes['class']
  }
}

export interface NSelectScrollDownButtonProps extends ScrollDownButtonExtensions {
  una?: {
    selectScrollDownButton?: HTMLAttributes['class']
    selectScrollDownButtonIcon?: HTMLAttributes['class']
  }
}

export interface NSelectScrollUpButtonProps extends ScrollUpButtonExtensions {
  una?: {
    selectScrollUpButton?: HTMLAttributes['class']
    selectScrollUpButtonIcon?: HTMLAttributes['class']
  }
}

export interface NSelectContentProps extends ContentExtensions {
  _selectScrollDownButton?: NSelectScrollDownButtonProps
  _selectScrollUpButton?: NSelectScrollUpButtonProps
  _selectSeparator?: NSelectSeparator
  una?: {
    selectContent?: HTMLAttributes['class']
  }
}

export interface NSelectItemIndicatorProps extends SelectItemIndicatorProps {
  icon?: HTMLAttributes['class']
  class?: HTMLAttributes['class']
  una?: {
    selectItemIndicator?: HTMLAttributes['class']
    selectItemIndicatorIcon?: HTMLAttributes['class']
  }
}

export interface NSelectItemProps extends ItemExtensions {
  selectItem?: HTMLAttributes['class']
  isSelected?: boolean
  _selectItemText?: NSelectItemTextProps
  _selectItemIndicator?: NSelectItemIndicatorProps
  una?: {
    selectItem?: HTMLAttributes['class']

    selectItemIndicatorWrapper?: HTMLAttributes['class']
  }
}

export interface NSelectItemTextProps extends ItemTextExtensions {
  una?: {
    selectItemText?: HTMLAttributes['class']
  }
}

export interface NSelectGroupProps extends GroupExtensions {
  una?: {
    selectGroup?: HTMLAttributes['class']
  }
}

export interface NSelectLabelProps extends LabelExtensions {
  una?: {
    selectLabel?: HTMLAttributes['class']
  }
}

export interface NSelectSeparator extends SeparatorExtensions {
  una?: {
    selectSeparator?: HTMLAttributes['class']
  }
}

Components

Select.vue
SelectContent.vue
SelectGroup.vue
SelectItem.vue
SelectItemText.vue
SelectLabel.vue
SelectRoot.vue
SelectScrollDownButton.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 { reactivePick } from '@vueuse/core'
import { useForwardPropsEmits } from 'reka-ui'
import { isEqualObject } from '../../../utils'
import SelectContent from './SelectContent.vue'
import SelectGroup from './SelectGroup.vue'
import SelectItem from './SelectItem.vue'
import SelectLabel from './SelectLabel.vue'
import SelectRoot from './SelectRoot.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 rootProps = reactivePick(props, [
  'modelValue',
  'defaultValue',
  'multiple',
  'disabled',
])
const forwarded = useForwardPropsEmits(rootProps, 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 }"
    v-bind="forwarded"
  >
    <SelectTrigger
      :id
      :size
      :status
      :select
      v-bind="props._selectTrigger"
    >
      <slot name="trigger" :model-value :open="open">
        <SelectValue
          :placeholder="props.placeholder"
          v-bind="props._selectValue"
          :aria-label="formatSelectedValue(modelValue)"
          :data-status="status"
        >
          <slot name="value" :model-value :open>
            {{ formatSelectedValue(modelValue) || props.placeholder }}
          </slot>
        </SelectValue>
      </slot>
    </SelectTrigger>

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

          <template
            v-for="item in items"
            :key="item"
          >
            <SelectItem
              :value="item"
              :size="size"
              v-bind="props._selectItem"
              :is-selected="isItemSelected(item, modelValue)"
            >
              <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"
          >
            <SelectSeparator
              v-if="i > 0"
              v-bind="props._selectSeparator"
            />

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

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