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.
Preview
Code

Group Items

PropDefaultTypeDescription
groupItems-booleanEnable support for group items.
Preview
Code

Objects

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

PropDefaultTypeDescription
valueAttribute-stringThe attribute value to be displayed in the select.
itemAttribute-stringThe attribute value to be displayed in the item.
Preview
Code
Output:

Form Group

You can use the select component inside the form-group component, or you can use it with the label component.

Preview
Code
Select a contributor from the Vue community

This field is required

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.

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]:line-clamp-1
  'select-trigger-trailing-icon': 'i-lucide-chevrons-up-down !text-1.042em',
  'select-trigger-trailing': 'ml-auto',
  'select-trigger-leading': '',

  'select-value': 'h-1.5em',

  '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-[--radix-select-trigger-height] w-full min-w-[--radix-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 { SelectContentProps, SelectGroupProps, SelectItemIndicatorProps, SelectItemProps, SelectItemTextProps, SelectLabelProps, SelectRootProps, SelectScrollDownButtonProps, SelectScrollUpButtonProps, SelectSeparatorProps, SelectTriggerProps, SelectValueProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'

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

type RootExtensions = Omit<SelectRootProps, 'modelValue' > & 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 = Omit<SelectItemProps, 'value'> & BaseExtensions
type ItemTextExtensions = SelectItemTextProps & BaseExtensions
type GroupExtensions = SelectGroupProps & BaseExtensions
type LabelExtensions = SelectLabelProps & BaseExtensions
type SeparatorExtensions = SelectSeparatorProps & BaseExtensions
type SelectExtensions = NSelectRootProps
  & BaseExtensions
  & Pick<NSelectItemProps, 'selectItem'>
  & Pick<NSelectTriggerProps, 'status' | 'select'>

export interface NSelectProps extends SelectExtensions {
  /**
   * The unique id of the select.
   */
  id?: string
  /**
   * The attribute name to use to display in the select items.
   *
   */
  itemAttribute?: string | number
  /**
   * The attribute name to use to display in the selected value.
   */
  valueAttribute?: string | number
  /**
   * The placeholder to display when no value is selected.
   */
  placeholder?: string
  /**
   * The label to display above the select items.
   */
  label?: string
  /**
   * The items to display in the select.
   *
   * @default []
   */
  items: any[]
  /**
   * Allows for multiple groups within the select.
   *
   * @default false
   * @example
   * items: [
   *  { label: 'Group 1', items: [1, 2, 3] },
   * ]
   */
  groupItems?: boolean

  // sub-components
  _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 {
  /**
   * 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
  }
}

export interface NSelectItemProps extends ItemExtensions {
  value: any
  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 setup lang="ts">
import type { SelectRootEmits } from 'radix-vue'
import type { NSelectProps } from '../../../types'
import {
  useForwardPropsEmits,
} from 'radix-vue'
import { computed, provide } from 'vue'
import { isEqualObject, omitProps } 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>(), {
  size: 'sm',
})

const emits = defineEmits<SelectRootEmits>()

const modelValue = defineModel<any>('modelValue')

const delegatedProps = computed(() => {
  const { class: _, ...delegated } = props

  return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)

const transformerValue = computed(() => {
  if (typeof modelValue.value === 'object') {
    if (forwarded.value.valueAttribute)
      return modelValue.value[forwarded.value.valueAttribute]

    if (forwarded.value.itemAttribute)
      return modelValue.value[forwarded.value.itemAttribute]
  }

  return modelValue.value
})

provide('selectModelValue', modelValue)
</script>

<template>
  <SelectRoot
    v-bind="omitProps(forwarded, ['items', 'groupItems', 'itemAttribute', 'placeholder', 'label', 'id', 'select'])"
    :model-value="transformerValue"
  >
    <SelectTrigger
      :id
      :size
      :status
      :select
      v-bind="forwarded._selectTrigger"
    >
      <slot name="trigger" :value="modelValue">
        <SelectValue
          v-bind="forwarded._selectValue"
          :placeholder="forwarded._selectValue?.placeholder || forwarded.placeholder"
        >
          <slot name="value" :value="modelValue">
            {{ transformerValue }}
          </slot>
        </SelectValue>
      </slot>
    </SelectTrigger>

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

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

        <!-- multiple-group -->
        <template
          v-else
        >
          <SelectGroup
            v-for="(groupItems, i) in items"
            :key="i"
            v-bind="props._selectGroup"
          >
            <SelectSeparator
              v-if="i > 0"
              v-bind="props._selectSeparator"
            />

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

              <template
                v-for="groupItem in groupItems.items"
                :key="groupItem"
              >
                <SelectItem
                  :value="groupItem"
                  :size
                  v-bind="{ ...forwarded._selectItem, ...groupItems?._selectItem, ...groupItem._selectItem }"
                  :is-selected="groupItem === transformerValue"
                >
                  <slot name="item" :item="groupItem">
                    {{ props.itemAttribute ? groupItem[props.itemAttribute] : groupItem }}
                  </slot>
                </SelectItem>
              </template>
            </slot>
          </SelectGroup>
        </template>
        <slot />
      </slot>
    </SelectContent>
  </SelectRoot>
</template>