Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
items | - | T[] | NComboboxGroupProps<ExtractItemType<T>>[] | The items to display in the combobox. |
modelValue | - | AcceptableValue | AcceptableValue[] | The controlled value of the listbox. Can be binded with with v-model . |
disabled | - | boolean | When true, prevents the user from interacting with the combobox. |
open | - | boolean | The controlled open state of the combobox. Can be binded with v-model . |
label | - | string | The heading to display for the grouped item. |
labelKey | label | string | The key name to use to display in the select items. |
valueKey | value | string | The key name to use to display in the selected value. |
textEmpty | No items found. | string | The text to display when the combobox is empty. |
by | - | string , ((a: AcceptableValue, b: AcceptableValue) => boolean) | Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. |
The T
generic extends AcceptableValue
from Reka UI. When using grouped items, the item type is automatically extracted.
Preview
Code
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<NCombobox
v-model="selectedFramework"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
by="value"
text-empty="No frameworks found."
/>
</template>
Read more in Reka Combobox Root API.
Multiple
Allow users to select multiple items from the list.
Prop | Default | Type | Description |
---|---|---|---|
multiple | false | boolean | When true, allows the user to select multiple items. |
Preview
Code
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<NCombobox
v-model="selectedFramework"
multiple
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
by="value"
/>
</template>
Trigger
Add a custom trigger content.
Prop | Default | Type | Description |
---|---|---|---|
_comboboxTrigger | { btn: 'solid-white', trailing: 'i-lucide-chevrons-up-down' } | NComboboxTriggerProps | The button props for the trigger, you can refer to the Button component for more details. |
Preview
Code
<script setup lang="ts">
const users = [
{ id: '1', username: 'shadcn' },
{ id: '2', username: 'leerob' },
{ id: '3', username: 'evilrabbit' },
]
const selectedUser = ref()
</script>
<template>
<NCombobox
v-model="selectedUser"
:items="users"
by="username"
:_combobox-input="{
placeholder: 'Select user...',
}"
class="flex"
>
<template #trigger>
<template v-if="selectedUser">
<div class="flex items-center gap-2">
<NAvatar
:src="`https://github.com/${selectedUser.username}.png`"
:alt="selectedUser.username"
square="5"
/>
{{ selectedUser.username }}
</div>
</template>
<template v-else>
Select user...
</template>
</template>
<template #label="{ item }">
<NAvatar
square="5"
:src="`https://github.com/${item.username}.png`"
:alt="item.username"
/>
{{ item.username }}
</template>
<template #footer>
<NComboboxSeparator />
<NComboboxGroup>
<NComboboxItem :value="null">
<NIcon name="i-lucide-plus-circle" />
Create user
</NComboboxItem>
</NComboboxGroup>
</template>
</NCombobox>
</template>
Read more in Button component
Read more in Reka Combobox Trigger API
List / Content
Prop | Default | Type | Description |
---|---|---|---|
_comboboxList | { align: 'center', sideOffset: 4, position: 'popper' } | NComboboxListProps | Props for customizing the dropdown list of the combobox. Controls alignment, offset distance from trigger, positioning behavior and more. |
Preview
Code
Create a new project
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<div className="flex w-full flex-col items-start justify-between rounded-md border px-4 py-3 sm:flex-row sm:items-center">
<NCombobox
v-model="selectedFramework"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
:_combobox-list="{
align: 'start',
side: 'right',
}"
by="value"
text-empty="No frameworks found."
>
<template #trigger>
<template v-if="selectedFramework">
{{ selectedFramework.label }}
</template>
<template v-else>
Select framework...
</template>
</template>
</NCombobox>
<p className="text-sm font-medium leading-none">
<span v-if="selectedFramework" className="mr-2 rounded-lg bg-primary px-2 py-1 text-xs text-inverted">
{{ selectedFramework.label }}
</span>
<span className="text-muted">Create a new project</span>
</p>
</div>
</template>
Read more in Reka Combobox Content API
Group
Props | Default | Type | Description |
---|
Preview
Code
<script setup lang="ts">
const timezones = [
{
label: 'Americas',
items: [
{ value: 'America/New_York', label: '(GMT-5) New York' },
{ value: 'America/Los_Angeles', label: '(GMT-8) Los Angeles' },
{ value: 'America/Chicago', label: '(GMT-6) Chicago' },
{ value: 'America/Toronto', label: '(GMT-5) Toronto' },
{ value: 'America/Vancouver', label: '(GMT-8) Vancouver' },
{ value: 'America/Sao_Paulo', label: '(GMT-3) São Paulo' },
],
},
{
label: 'Europe',
items: [
{ value: 'Europe/London', label: '(GMT+0) London' },
{ value: 'Europe/Paris', label: '(GMT+1) Paris' },
{ value: 'Europe/Berlin', label: '(GMT+1) Berlin' },
{ value: 'Europe/Rome', label: '(GMT+1) Rome' },
{ value: 'Europe/Madrid', label: '(GMT+1) Madrid' },
{ value: 'Europe/Amsterdam', label: '(GMT+1) Amsterdam' },
],
},
{
label: 'Asia/Pacific',
items: [
{ value: 'Asia/Tokyo', label: '(GMT+9) Tokyo' },
{ value: 'Asia/Shanghai', label: '(GMT+8) Shanghai' },
{ value: 'Asia/Singapore', label: '(GMT+8) Singapore' },
{ value: 'Asia/Dubai', label: '(GMT+4) Dubai' },
{ value: 'Australia/Sydney', label: '(GMT+11) Sydney' },
{ value: 'Asia/Seoul', label: '(GMT+9) Seoul' },
],
},
]
type Timezone = typeof timezones[0]
const selectedTimezone = ref<Timezone['items'][number]>(timezones[0].items[0])
const selectedGroup = computed(() => timezones.find(group => group.items.find(tz => tz.value === selectedTimezone.value?.value)))
</script>
<template>
<NCombobox
v-model="selectedTimezone"
:items="timezones"
by="value"
:_combobox-input="{
placeholder: 'Select timezone...',
}"
:_combobox-list="{
class: 'w-300px',
align: 'start',
}"
:_combobox-viewport="{
class: 'max-h-260px',
}"
:_combobox-trigger="{
class: 'h-12 px-2.5',
}"
class="flex"
>
<template #trigger>
<template v-if="selectedTimezone">
<div class="flex flex-col items-start gap-0.5">
<span class="text-xs font-normal opacity-75">
{{ selectedGroup?.label }}
</span>
<span>{{ selectedTimezone.label }}</span>
</div>
</template>
<template v-else>
Select timezone...
</template>
</template>
</NCombobox>
</template>
Read more in Reka Combobox Group Items API
Form Field
Use the NFormField
component to create a form field.
Preview
Code
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
const formSchema = toTypedSchema(z.object({
framework: z.object({
value: z.string().min(1, 'This field is required'),
label: z.string().min(1, 'This field is required'),
}),
}))
useForm({
validationSchema: formSchema,
validateOnMount: true,
})
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<form class="flex">
<NFormField
name="framework"
label="Framework"
description="Select a framework without a trigger"
>
<NCombobox
v-model="selectedFramework"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
by="value"
/>
</NFormField>
</form>
</template>
Read more in Form component
Size
Adjust the combobox 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.
Prop | Default | Type | Description |
---|---|---|---|
size | sm | string | Adjusts the overall size of the combobox component. |
_comboboxInput.size | sm | string | Customizes the size of the combobox input element. |
_comboboxItem.size | sm | string | Customizes the size of each item within the combobox dropdown. |
_comboboxTrigger.size | sm | string | Modifies the size of the combobox trigger element. |
Preview
Code
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<div class="flex flex-wrap items-center gap-4">
<NCombobox
v-model="selectedFramework"
size="xs"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
class="flex"
by="value"
>
<template #trigger="{ modelValue }">
<template v-if="modelValue">
{{ modelValue }}
</template>
<template v-else>
Select Framework
</template>
</template>
</NCombobox>
<NCombobox
v-model="selectedFramework"
size="sm"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
class="flex"
by="value"
>
<template #trigger="{ modelValue }">
<template v-if="modelValue">
{{ modelValue }}
</template>
<template v-else>
Select Framework
</template>
</template>
</NCombobox>
<NCombobox
v-model="selectedFramework"
size="md"
:items="frameworks"
:_combobox-input="{
placeholder: 'Select framework...',
autocomplete: 'off',
}"
class="flex"
by="value"
>
<template #trigger="{ modelValue }">
<template v-if="modelValue">
{{ modelValue }}
</template>
<template v-else>
Select Framework
</template>
</template>
</NCombobox>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | Slot for advanced custom rendering using sub-components. |
trigger | modelValue , open | Custom content inside the default trigger button. |
trigger-wrapper | modelValue , open | Completely replace the default trigger button/component. |
input-wrapper | modelValue , open | Completely replace the default input component. |
item | item , selected | Custom rendering for the entire content of each combobox item. |
label | item | Custom rendering for the label text within each item. |
indicator | item | Custom rendering for the selection indicator within each item. |
header | - | Content rendered inside the list, before the items. |
body | - | Completely replace the default item list container (Viewport ). |
footer | - | Content rendered inside the list, after the items. |
Custom Rendering
Use the default
slot for full control over the combobox's structure. This allows you to compose the combobox using its individual sub-components (like ComboboxInput
, ComboboxList
, etc., listed in the Components section), similar to libraries like shadcn/ui
.
Preview
Code
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFramework = ref()
</script>
<template>
<NCombobox
v-model="selectedFramework"
by="label"
>
<NComboboxAnchor as-child>
<NComboboxTrigger class="w-[200px]">
{{ selectedFramework?.label ?? 'Select framework...' }}
</NComboboxTrigger>
</NComboboxAnchor>
<NComboboxList>
<NComboboxInput
class="border-0 border-b-1 rounded-none placeholder:text-gray-500 focus-visible:ring-0"
placeholder="Select framework..."
/>
<NComboboxEmpty>
No framework found.
</NComboboxEmpty>
<NComboboxGroup>
<NComboboxItem
v-for="framework in frameworks"
:key="framework.value"
:value="framework"
>
{{ framework.label }}
<NComboboxItemIndicator>
<NIcon name="i-lucide-check" />
</NComboboxItemIndicator>
</NComboboxItem>
</NComboboxGroup>
</NComboboxList>
</NCombobox>
</template>
Custom Multiple Selection
Customize the multiple selection content.
Preview
Code
<script setup lang="ts">
const frameworks = [
{ value: 'next.js', label: 'Next.js' },
{ value: 'sveltekit', label: 'SvelteKit' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
]
const selectedFrameworks = ref([])
</script>
<template>
<NCombobox
v-model="selectedFrameworks"
:items="frameworks"
by="value"
multiple
:_combobox-input="{
placeholder: 'Select frameworks...',
}"
:_combobox-list="{
class: 'w-300px',
align: 'start',
}"
class="flex"
>
<template #trigger>
{{ selectedFrameworks?.length > 0
? selectedFrameworks.map(val => {
const framework = frameworks.find(f => f.value === val)
return framework ? framework.label : val
}).join(", ")
: "Select frameworks..." }}
</template>
<template #item="{ item, selected }">
<NCheckbox
:model-value="selected"
tabindex="-1"
aria-hidden="true"
/>
{{ item.label }}
</template>
</NCombobox>
</template>
Presets
shortcuts/combobox.ts
type ComboboxPrefix = 'combobox'
export const staticCombobox: Record<`${ComboboxPrefix}-${string}` | ComboboxPrefix, string> = {
// base
'combobox': 'flex',
'combobox-trigger-info-icon': 'i-info',
'combobox-trigger-error-icon': 'i-error',
'combobox-trigger-success-icon': 'i-success',
'combobox-trigger-warning-icon': 'i-warning',
'combobox-trigger-trailing-icon': 'i-lucide-chevrons-up-down',
'combobox-input-leading-icon': 'i-lucide-search',
'combobox-trigger': 'px-0.8571428571428571em w-full justify-between font-normal [&>span]:truncate',
'combobox-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) rtl:mr-auto ltr:ml-auto',
'combobox-trigger-leading': 'size-1.1428571428571428em',
'combobox-item': 'data-[highlighted]:bg-accent data-[highlighted]:text-accent relative flex cursor-default items-center gap-2 rounded-sm px-0.5714285714285714em py-0.42857142857142855em text-sm outline-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
'combobox-item-indicator': 'ml-auto',
'combobox-item-indicator-icon': '',
'combobox-item-indicator-icon-name': 'i-check',
'combobox-anchor': 'min-w-200px',
'combobox-empty': 'py-1.7142857142857142em text-center text-0.8571428571428571em',
'combobox-group': 'overflow-hidden p-0.2857142857142857em text-foreground',
'combobox-label': 'px-0.6666666666666666em py-0.5em text-0.8571428571428571em text-muted font-medium',
'combobox-list': 'z-50 w-[--reka-popper-anchor-width] rounded-md border bg-popover text-popover overflow-hidden shadow-md outline-none 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',
'combobox-separator': 'bg-border -mx-1 h-px',
'combobox-viewport': 'max-h-300px scroll-py-1 overflow-x-hidden overflow-y-auto',
}
export const dynamicCombobox: [RegExp, (params: RegExpExecArray) => string][] = [
// dynamic preset
]
export const combobox = [
...dynamicCombobox,
staticCombobox,
]
Props
types/combobox.ts
import type { AcceptableValue, ComboboxAnchorProps, ComboboxContentProps, ComboboxEmptyProps, ComboboxGroupProps, ComboboxInputProps, ComboboxItemIndicatorProps, ComboboxItemProps, ComboboxLabelProps, ComboboxPortalProps, ComboboxRootProps, ComboboxSeparatorProps, ComboboxTriggerProps, ComboboxViewportProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
import type { NCheckboxProps } from './checkbox'
import type { NInputProps } from './input'
interface BaseExtensions {
class?: HTMLAttributes['class']
size?: HTMLAttributes['class']
}
// Extract the actual item type when dealing with grouped items
export type ExtractItemType<T> = T extends { items: infer I extends AcceptableValue[] } ? I[number] : T
export interface NComboboxProps<T extends AcceptableValue> extends Omit<ComboboxRootProps<ExtractItemType<T>>, 'modelValue'>, Pick<NComboboxInputProps, 'status' | 'id'>, BaseExtensions {
/**
* The model value for the combobox.
* When using grouped items, this will be the item type from within the groups.
*/
modelValue?: ExtractItemType<T> | ExtractItemType<T>[] | null | undefined
/**
* The items to display in the combobox.
*
* @default []
*/
items?: T[] | NComboboxGroupProps<ExtractItemType<T>>[]
/**
* The key name to use to display in the select items.
*
* @default 'label'
*/
labelKey?: keyof ExtractItemType<T>
/**
* The key name to use to display in the selected value.
*
* @default 'value'
*/
valueKey?: keyof ExtractItemType<T>
/**
* Whether to show a separator between groups.
*
* @default false
*/
groupSeparator?: boolean
/**
* The text to display when the combobox is empty.
*
* @default 'No items found.'
*/
textEmpty?: string
/**
* The heading to display for the grouped item.
*
* @default ''
*/
label?: string
/**
* Sub-component configurations
*/
_comboboxAnchor?: NComboboxAnchorProps
_comboboxEmpty?: NComboboxEmptyProps
_comboboxGroup?: NComboboxGroupProps<ExtractItemType<T>>
_comboboxInput?: NComboboxInputProps
_comboboxItem?: NComboboxItemProps<ExtractItemType<T>>
_comboboxItemIndicator?: NComboboxItemIndicatorProps
_comboboxLabel?: NComboboxLabelProps
_comboboxList?: NComboboxListProps
_comboboxSeparator?: NComboboxSeparatorProps
_comboboxTrigger?: NComboboxTriggerProps
_comboboxViewport?: NComboboxViewportProps
_comboboxCheckbox?: NCheckboxProps
_comboboxContent?: ComboboxContentProps
_comboboxPortal?: ComboboxPortalProps
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/combobox.ts
*/
una?: NComboboxUnaProps
}
export interface NComboboxLabelProps extends ComboboxLabelProps, BaseExtensions {
label?: string
una?: Pick<NComboboxUnaProps, 'comboboxLabel'>
}
export interface NComboboxItemProps<T> extends ComboboxItemProps<T>, BaseExtensions {
una?: Pick<NComboboxUnaProps, 'comboboxItem'>
}
export interface NComboboxAnchorProps extends ComboboxAnchorProps, BaseExtensions {
una?: Pick<NComboboxUnaProps, 'comboboxAnchor'>
}
export interface NComboboxEmptyProps extends ComboboxEmptyProps, BaseExtensions {
una?: Pick<NComboboxUnaProps, 'comboboxEmpty'>
}
export interface NComboboxGroupProps<T extends AcceptableValue> extends ComboboxGroupProps, BaseExtensions {
label?: string
items?: T[]
_comboboxItem?: Partial<NComboboxItemProps<T>>
_comboboxLabel?: Partial<NComboboxLabelProps>
una?: Pick<NComboboxUnaProps, 'comboboxGroup' | 'comboboxLabel'>
}
export interface NComboboxInputProps extends ComboboxInputProps, Omit<NInputProps, 'modelValue'> {
[key: string]: any
}
export interface NComboboxItemIndicatorProps extends ComboboxItemIndicatorProps, BaseExtensions {
icon?: HTMLAttributes['class']
una?: Pick<NComboboxUnaProps, 'comboboxItemIndicator' | 'comboboxItemIndicatorIcon'>
}
export interface NComboboxListProps extends ComboboxContentProps, BaseExtensions {
viewportClass?: HTMLAttributes['class']
una?: Pick<NComboboxUnaProps, 'comboboxList'>
_comboboxPortal?: ComboboxPortalProps
}
export interface NComboboxSeparatorProps extends ComboboxSeparatorProps, BaseExtensions {
una?: Pick<NComboboxUnaProps, 'comboboxSeparator'>
}
export interface NComboboxTriggerProps extends ComboboxTriggerProps, NButtonProps {
/**
* The unique id of the select trigger to be used for the form field.
*/
id?: 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/combobox.ts
*/
una?: Pick<NComboboxUnaProps, 'comboboxTrigger' | 'comboboxTriggerLeading' | 'comboboxTriggerTrailing' | 'comboboxTriggerInfoIcon' | 'comboboxTriggerSuccessIcon' | 'comboboxTriggerWarningIcon' | 'comboboxTriggerErrorIcon'> & NButtonProps['una']
}
export interface NComboboxViewportProps extends ComboboxViewportProps, BaseExtensions {
una?: Pick<NComboboxUnaProps, 'comboboxViewport'>
}
export interface NComboboxUnaProps {
combobox?: HTMLAttributes['class']
comboboxAnchor?: HTMLAttributes['class']
comboboxLabel?: HTMLAttributes['class']
comboboxItem?: HTMLAttributes['class']
comboboxItemIndicator?: HTMLAttributes['class']
comboboxItemIndicatorIcon?: HTMLAttributes['class']
comboboxSeparator?: HTMLAttributes['class']
comboboxViewport?: HTMLAttributes['class']
comboboxEmpty?: HTMLAttributes['class']
comboboxGroup?: HTMLAttributes['class']
comboboxList?: HTMLAttributes['class']
comboboxTrigger?: HTMLAttributes['class']
comboboxTriggerLeading?: HTMLAttributes['class']
comboboxTriggerTrailing?: HTMLAttributes['class']
comboboxTriggerInfoIcon?: HTMLAttributes['class']
comboboxTriggerSuccessIcon?: HTMLAttributes['class']
comboboxTriggerWarningIcon?: HTMLAttributes['class']
comboboxTriggerErrorIcon?: HTMLAttributes['class']
}
Components
Combobox.vue
ComboboxAnchor.vue
ComboboxTrigger.vue
ComboboxInput.vue
ComboboxList.vue
ComboboxViewport.vue
ComboboxEmpty.vue
ComboboxGroup.vue
ComboboxLabel.vue
ComboboxItem.vue
ComboboxItemIndicator.vue
ComboboxSeparator.vue
<script lang="ts">
import type { AcceptableValue, ComboboxRootEmits } from 'reka-ui'
import type { ExtractItemType, NComboboxGroupProps, NComboboxProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '../../utils'
</script>
<script setup lang="ts" generic="T extends AcceptableValue">
import { computed } from 'vue'
import ComboboxAnchor from './ComboboxAnchor.vue'
import ComboboxEmpty from './ComboboxEmpty.vue'
import ComboboxGroup from './ComboboxGroup.vue'
import ComboboxInput from './ComboboxInput.vue'
import ComboboxItem from './ComboboxItem.vue'
import ComboboxItemIndicator from './ComboboxItemIndicator.vue'
import ComboboxList from './ComboboxList.vue'
import ComboboxSeparator from './ComboboxSeparator.vue'
import ComboboxTrigger from './ComboboxTrigger.vue'
import ComboboxViewport from './ComboboxViewport.vue'
const props = withDefaults(defineProps<NComboboxProps<T>>(), {
textEmpty: 'No items found.',
size: 'md',
})
const emits = defineEmits<ComboboxRootEmits<ExtractItemType<T>>>()
const rootProps = reactiveOmit(props, [
'items',
'una',
'size',
'label',
'labelKey',
'valueKey',
'groupSeparator',
'textEmpty',
'_comboboxAnchor',
'_comboboxEmpty',
'_comboboxGroup',
'_comboboxInput',
'_comboboxItem',
'_comboboxItemIndicator',
'_comboboxLabel',
'_comboboxList',
'_comboboxSeparator',
'_comboboxTrigger',
'_comboboxViewport',
'_comboboxCheckbox',
])
const forwarded = useForwardPropsEmits(rootProps, emits)
const labelKey = computed(() => props.labelKey?.toString() ?? 'label')
const valueKey = computed(() => props.valueKey?.toString() ?? 'value')
// Check if items are grouped
const hasGroups = computed(() => {
return Array.isArray(props.items) && props.items.length > 0
&& typeof props.items[0] === 'object' && 'items' in (props.items[0] as any)
})
// Helper function to safely get a property from an item
function getItemProperty<K extends string>(item: ExtractItemType<T> | null | undefined, key: K): any {
if (item == null)
return ''
return typeof item !== 'object' ? item : (item as Record<K, unknown>)[key]
}
// Find a matching item from the items list by its value
function findItemByValue(value: unknown): ExtractItemType<T> | undefined {
if (!props.items)
return undefined
if (hasGroups.value) {
// Search in grouped items
for (const group of props.items as NComboboxGroupProps<ExtractItemType<T>>[]) {
const found = group.items?.find(item => getItemProperty(item, valueKey.value) === value)
if (found)
return found
}
return undefined
}
else {
// Search in flat items list
return (props.items as ExtractItemType<T>[]).find(item => getItemProperty(item, valueKey.value) === value)
}
}
// Display function that handles both single and multiple selections
function getDisplayValue(val: unknown): string {
// Handle empty values
if (!val || (Array.isArray(val) && val.length === 0))
return ''
// Handle multiple selection (array values)
if (props.multiple && Array.isArray(val)) {
return val.map((v) => {
// For primitive values (string/number), find matching item to get label
if (typeof v !== 'object' || v === null) {
const item = findItemByValue(v)
return item ? getItemProperty(item, labelKey.value) : v
}
// For objects, try to get the label directly
return getItemProperty(v, labelKey.value) || getItemProperty(v, valueKey.value) || ''
}).filter(Boolean).join(', ')
}
// For single primitive value
if (typeof val !== 'object' || val === null) {
const item = findItemByValue(val)
return item ? getItemProperty(item, labelKey.value) : String(val || '')
}
// For single object, get its label
return getItemProperty(val as any, labelKey.value) || getItemProperty(val as any, valueKey.value) || ''
}
// Check if an item is selected in the current modelValue
function isItemSelected(item: ExtractItemType<T> | null | undefined): boolean {
if (item == null)
return false
const itemValue = getItemProperty(item, valueKey.value)
// For multiple selection
if (props.multiple && Array.isArray(props.modelValue)) {
return props.modelValue.includes(itemValue)
}
// For single selection
return typeof props.modelValue === 'object' && props.modelValue !== null
? getItemProperty(props.modelValue as ExtractItemType<T>, valueKey.value) === itemValue
: props.modelValue === itemValue
}
</script>
<template>
<ComboboxRoot
v-slot="{ modelValue, open }"
data-slot="combobox"
:class="cn(
'combobox',
props.una?.combobox,
props.class,
)"
v-bind="forwarded"
>
<slot>
<ComboboxAnchor
v-bind="props._comboboxAnchor"
:una
>
<slot name="anchor">
<template
v-if="$slots.trigger || $slots.triggerRoot"
>
<slot name="trigger-wrapper">
<ComboboxTrigger
v-bind="props._comboboxTrigger"
:id
:status
:class="cn(
'text-0.875em',
props._comboboxTrigger?.class,
)"
:size
>
<slot name="trigger" :model-value :open />
</ComboboxTrigger>
</slot>
</template>
<template v-else>
<slot name="input-wrapper" :model-value :open>
<ComboboxInput
:id
:display-value="(val: unknown) => getDisplayValue(val)"
name="frameworks"
:status
:size
v-bind="props._comboboxInput"
/>
</slot>
</template>
</slot>
</ComboboxAnchor>
<ComboboxList
v-bind="{ ...props._comboboxList, ...props._comboboxContent }"
:_combobox-portal
:size
:una
>
<slot name="list">
<slot name="input-wrapper" :model-value :open>
<ComboboxInput
v-if="$slots.trigger || $slots.triggerRoot"
:size
:class="cn(
'border-0 border-b-1 rounded-none focus-visible:ring-0',
props._comboboxInput?.class,
)"
v-bind="props._comboboxInput"
/>
</slot>
<slot name="header" />
<slot name="body">
<ComboboxViewport
v-bind="props._comboboxViewport"
:una
>
<ComboboxEmpty
v-bind="props._comboboxEmpty"
:una
>
<slot name="empty">
{{ props.textEmpty }}
</slot>
</ComboboxEmpty>
<!-- Non-grouped items -->
<template v-if="!hasGroups">
<ComboboxGroup
v-bind="props._comboboxGroup"
:label="props.label"
:una
>
<slot name="group">
<ComboboxItem
v-for="item in items as ExtractItemType<T>[]"
:key="getItemProperty(item, valueKey)"
:value="props.multiple ? getItemProperty(item, valueKey) : item"
:size
v-bind="props._comboboxItem"
:class="cn(
'text-0.875em',
props._comboboxItem?.class,
)"
:una
>
<slot name="item" :item="item" :selected="isItemSelected(item)">
<slot name="label" :item="item">
{{ getItemProperty(item, labelKey) }}
</slot>
<ComboboxItemIndicator
v-bind="props._comboboxItemIndicator"
:una
>
<slot name="item-indicator" :item="item">
<NIcon name="i-lucide-check" />
</slot>
</ComboboxItemIndicator>
</slot>
</ComboboxItem>
</slot>
</ComboboxGroup>
</template>
<!-- Grouped items -->
<template v-else>
<ComboboxGroup
v-for="(group, i) in items as NComboboxGroupProps<ExtractItemType<T>>[]"
:key="i"
v-bind="props._comboboxGroup"
:label="group.label"
:una
>
<ComboboxSeparator
v-if="i > 0 && props.groupSeparator"
v-bind="props._comboboxSeparator"
:una
/>
<slot name="group" :group="group">
<ComboboxItem
v-for="item in group.items"
:key="getItemProperty(item, valueKey)"
:value="props.multiple ? getItemProperty(item, valueKey) : item"
:size
v-bind="{ ...props._comboboxItem, ...group._comboboxItem }"
:class="cn(
'text-0.875em',
props._comboboxItem?.class,
)"
:una
>
<slot name="item" :item="item" :group="group" :selected="isItemSelected(item)">
<slot name="label" :item="item">
{{ getItemProperty(item, labelKey) }}
</slot>
<ComboboxItemIndicator
v-bind="props._comboboxItemIndicator"
:una
>
<slot name="indicator" :item="item" />
</ComboboxItemIndicator>
</slot>
</ComboboxItem>
</slot>
</ComboboxGroup>
</template>
</ComboboxViewport>
</slot>
<slot name="footer" />
</slot>
</ComboboxList>
</slot>
</ComboboxRoot>
</template>
<script setup lang="ts">
import type { NComboboxAnchorProps } from '../../types'
import { ComboboxAnchor, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NComboboxAnchorProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ComboboxAnchor
data-slot="combobox-anchor"
v-bind="forwarded"
:class="cn(
'combobox-anchor',
props.una?.comboboxAnchor,
props.class,
)"
>
<slot />
</ComboboxAnchor>
</template>
<script setup lang="ts">
import type { NComboboxTriggerProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxTrigger, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn, randomId } from '../../utils'
import Button from '../elements/Button.vue'
import Icon from '../elements/Icon.vue'
const props = withDefaults(defineProps<NComboboxTriggerProps>(), {
btn: 'solid-white',
})
const forwardedProps = useForwardProps(reactiveOmit(props, 'class', 'status', 'una', 'btn'))
const statusClassVariants = computed(() => {
const btn = {
info: 'btn-outline-info',
success: 'btn-outline-success',
warning: 'btn-outline-warning',
error: 'btn-outline-error',
default: undefined,
}
const icon = {
info: props.una?.comboboxTriggerInfoIcon ?? 'combobox-trigger-info-icon',
success: props.una?.comboboxTriggerSuccessIcon ?? 'combobox-trigger-success-icon',
warning: props.una?.comboboxTriggerWarningIcon ?? 'combobox-trigger-warning-icon',
error: props.una?.comboboxTriggerErrorIcon ?? 'combobox-trigger-error-icon',
default: props?.trailing ?? 'combobox-trigger-trailing-icon',
}
return {
btn: btn[props.status ?? 'default'],
icon: icon[props.status ?? 'default'],
}
})
const id = computed(() => props.id ?? randomId('combobox-trigger'))
const status = computed(() => props.status ?? 'default')
</script>
<template>
<ComboboxTrigger
v-bind="forwardedProps"
as-child
>
<slot name="wrapper">
<Button
:id
:btn="statusClassVariants.btn ? undefined : props.btn"
:data-status="status"
data-slot="combobox-trigger"
:class="cn(
'combobox-trigger',
props.class,
)"
tabindex="0"
:una="{
...props.una,
btn: props.una?.comboboxTrigger,
btnLeading: cn(
'combobox-trigger-leading',
props.una?.btnLeading,
props.una?.comboboxTriggerLeading,
),
btnDefaultVariant: statusClassVariants.btn,
}"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
<template #trailing>
<Icon
:data-status="status"
:name="statusClassVariants.icon"
:class="cn(
'combobox-trigger-trailing',
props.una?.btnTrailing,
)"
/>
</template>
</Button>
</slot>
</ComboboxTrigger>
</template>
<script setup lang="ts">
import type { ComboboxInputEmits } from 'reka-ui'
import type { NComboboxInputProps } from '../../types'
import { ComboboxInput, useForwardPropsEmits } from 'reka-ui'
import { inject, onMounted, ref } from 'vue'
import { isInComboboxListKey } from '../../utils/injectionKeys'
import Input from '../forms/Input.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NComboboxInputProps>(), {
leading: 'combobox-input-leading-icon',
})
const emits = defineEmits<ComboboxInputEmits>()
const forwarded = useForwardPropsEmits(props, emits)
const inputRef = ref<InstanceType<typeof Input>>()
const isInList = inject(isInComboboxListKey, false)
onMounted(() => {
if (isInList) {
inputRef.value?.focus()
}
})
</script>
<template>
<ComboboxInput
v-bind="props"
as-child
>
<Input
ref="inputRef"
data-slot="command-input"
v-bind="{ ...forwarded, ...$attrs }"
:_input-wrapper="{
'data-slot': 'command-input-wrapper',
}"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</Input>
</ComboboxInput>
</template>
<script setup lang="ts">
import type { ComboboxContentEmits } from 'reka-ui'
import type { NComboboxListProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { ComboboxContent, ComboboxPortal, useForwardPropsEmits } from 'reka-ui'
import { provide } from 'vue'
import { cn } from '../../utils'
import { isInComboboxListKey } from '../../utils/injectionKeys'
const props = withDefaults(defineProps<NComboboxListProps>(), {
position: 'popper',
align: 'center',
sideOffset: 4,
})
const emits = defineEmits<ComboboxContentEmits>()
provide(isInComboboxListKey, true)
const delegatedProps = reactiveOmit(props, 'class', 'viewportClass')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxPortal
v-bind="props._comboboxPortal"
>
<ComboboxContent
data-slot="combobox-list"
v-bind="forwarded"
:class="cn(
'origin-(--reka-combobox-content-transform-origin)',
'combobox-list',
props.una?.comboboxList,
props.class,
)"
>
<slot />
</ComboboxContent>
</ComboboxPortal>
</template>
<script setup lang="ts">
import type { NComboboxViewportProps } from '../../types'
import { ComboboxViewport, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NComboboxViewportProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ComboboxViewport
data-slot="combobox-viewport"
v-bind="forwarded"
:class="cn(
'combobox-viewport',
props.una?.comboboxViewport,
props.class,
)"
>
<slot />
</ComboboxViewport>
</template>
<script setup lang="ts">
import type { NComboboxEmptyProps } from '../../types'
import { ComboboxEmpty } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NComboboxEmptyProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxEmpty
data-slot="combobox-empty"
v-bind="delegatedProps"
:class="cn(
'combobox-empty',
props.una?.comboboxEmpty,
props.class,
)"
>
<slot />
</ComboboxEmpty>
</template>
<script lang="ts">
import type { AcceptableValue } from 'reka-ui'
</script>
<script setup lang="ts" generic="T extends AcceptableValue">
import type { NComboboxGroupProps } from '../../types'
import { ComboboxGroup } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
import ComboboxLabel from './ComboboxLabel.vue'
const props = defineProps<NComboboxGroupProps<T>>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxGroup
data-slot="combobox-group"
v-bind="delegatedProps"
:class="cn(
'combobox-group',
props.una?.comboboxGroup,
props.class,
)"
>
<ComboboxLabel
v-if="props.label"
:una
>
{{ props.label }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>
<script setup lang="ts">
import type { NComboboxLabelProps } from '../../types'
import { ComboboxLabel } from 'reka-ui'
import { cn } from '../../utils'
const props = defineProps<NComboboxLabelProps>()
</script>
<template>
<ComboboxLabel
:class="cn(
'combobox-label',
props.una?.comboboxLabel,
props.class,
)"
>
<slot>
{{ props.label }}
</slot>
</ComboboxLabel>
</template>
<script setup lang="ts" generic="T">
import type { ComboboxItemEmits } from 'reka-ui'
import type { NComboboxItemProps } from '../../types'
import { ComboboxItem, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NComboboxItemProps<T>>()
const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxItem
data-slot="combobox-item"
v-bind="forwarded"
:class="cn(
`combobox-item`,
props.una?.comboboxItem,
props.class,
)"
>
<slot />
</ComboboxItem>
</template>
<script setup lang="ts">
import type { NComboboxItemIndicatorProps } from '../../types'
import { ComboboxItemIndicator, useForwardProps } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = withDefaults(defineProps<NComboboxItemIndicatorProps>(), {
icon: 'combobox-item-indicator-icon-name',
})
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ComboboxItemIndicator
data-slot="combobox-item-indicator"
v-bind="forwarded"
:class="cn(
'combobox-item-indicator',
props.una?.comboboxItemIndicator,
props.class,
)"
>
<slot>
<NIcon
:name="props.icon"
:class="cn(
'combobox-item-indicator-icon',
props.una?.comboboxItemIndicatorIcon,
)"
/>
</slot>
</ComboboxItemIndicator>
</template>
<script setup lang="ts">
import type { NComboboxSeparatorProps } from '../../types'
import { ComboboxSeparator } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NComboboxSeparatorProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxSeparator
data-slot="combobox-separator"
v-bind="delegatedProps"
:class="cn(
'combobox-separator',
props.una?.comboboxSeparator,
props.class,
)"
>
<slot />
</ComboboxSeparator>
</template>