π’ Select
- Can be controlled or uncontrolled.
- Offers 2 positioning modes.
- Supports items, labels, groups of items.
- Focus is fully managed.
- Full keyboard navigation.
- Supports custom placeholder.
- Typeahead support.
- Supports Right to Left direction.
Basic
use NSelect
to create a range input.
Multiple Group
multiple-group="{value}"
- enable multiple group items.
Objects
Prop | Description |
---|---|
value-attribute | The attribute value to be displayed in the select. |
item-attribute | The attribute value to be displayed in the item. |
Output:
Label
You can use the
NFormGroup
component to create a label for the select.
Read more about the
NFormGroup
component here.Select a contributor from the Vue community
This field is required
Variant and Color
select="{variant}-{color}"
is used to set the variant of the select. The default variant is soft-black
.
select-item="{color}"
is used to set the variant of the select item. The default variant is soft-black
.
Prop | Description |
---|---|
select | Set the select variant and color. |
_selectTrigger.select | Set the select variant and color via _selectTrigger . |
NSelect
is wrapped around the NButton component. This means that all the props and slots of NButton
are available through the _selectTrigger
prop.Dynamic colors:Default color:
Disabled
Prop | Description |
---|---|
disabled | Disable the select. |
_selectItem.disabled | Disable the specific item. |
Size
Prop | Description |
---|---|
size | Set the select size. |
_selectItem.size | Set the item size. |
_selectTrigger.size | Set the trigger size. |
π You can freely adjust the size of the select using any size imaginable. No limits exist, and you aan use
breakpoints
such assm:sm, xs:lg
to change size based on screen size orstates
such ashover:lg, focus:3xl
to change size based on input state and more.
The
height
and width
of the select scale depends on the select-size
. If you want to change the height
and width
simultaneously, you can always customize it using utility classes.Slots
You can use the following slots to customize the select.
Name | Description | Props |
---|---|---|
trigger | The trigger slot. | value |
value | The value slot. | value |
content | The content slot. | items |
label | The label slot. | label |
item | The item slot. | item |
group | The group slot. | items |
Props
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
/**
* Enable multiple group items.
*
* @default false
*/
multipleGroup?: boolean
/**
* 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[]
// sub-components
_selectScrollUpButton?: Partial<NSelectScrollUpButtonProps>
_selectItemText?: Partial<NSelectItemTextProps>
_selectScrollDownButton?: Partial<NSelectScrollDownButtonProps>
_selectGroup?: Partial<NSelectGroupProps>
_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']
}
}
Presets
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 n-disabled',
'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,
]
Component
<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', 'multipleGroup', '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 :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="!forwarded.multipleGroup">
<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="forwarded._selectGroup"
>
<SelectSeparator
v-if="i > 0"
v-bind="forwarded._selectSeparator"
/>
<slot name="group" :items="groupItems">
<SelectLabel
v-if="groupItems.label"
:size="forwarded.size"
v-bind="{ ...forwarded._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="forwarded.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>