Combobox
Searchable select with an input or chips trigger. Base UI Combobox.
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList,} from "@/components/ui/combobox";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];
export default function Demo() { return ( <Combobox items={tokens}> <ComboboxInput placeholder="Select a token" showClear className="w-64" /> <ComboboxContent> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxList> {(item: string) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> );}<script setup lang="ts">import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxViewport,} from "@/components/ui/combobox";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];</script>
<template> <Combobox> <ComboboxInput placeholder="Select a token" show-clear class="w-64" /> <ComboboxList> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxViewport> <ComboboxItem v-for="token in tokens" :key="token" :value="token"> {{ token }} </ComboboxItem> </ComboboxViewport> </ComboboxList> </Combobox></template><script lang="ts"> import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "$lib/components/ui/combobox";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];
let value = $state(""); let search = $state(""); const filtered = $derived( tokens.filter((t) => t.toLowerCase().includes(search.toLowerCase())) );</script>
<Combobox type="single" bind:value> <ComboboxInput placeholder="Select a token" showClear class="w-64" oninput={(e) => (search = e.currentTarget.value)} /> <ComboboxContent> <ComboboxList> {#each filtered as token (token)} <ComboboxItem value={token} label={token} /> {:else} <ComboboxEmpty>No tokens found.</ComboboxEmpty> {/each} </ComboboxList> </ComboboxContent></Combobox>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/comboboxpnpm dlx shadcn@latest add @edmi-ui/comboboxyarn dlx shadcn@latest add @edmi-ui/comboboxbunx shadcn@latest add @edmi-ui/comboboxPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactAdd the Edmi components this one builds on:
button,input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/comboboxpnpm dlx shadcn-vue@latest add @edmi-ui/comboboxyarn dlx shadcn-vue@latest add @edmi-ui/comboboxbunx shadcn-vue@latest add @edmi-ui/comboboxPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreAdd the Edmi components this one builds on:
button,input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,input-group.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList,} from "@/components/ui/combobox";
<Combobox> <ComboboxInput placeholder="Select a token" showClear className="w-64" /> <ComboboxContent> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxList> {(item: string) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent></Combobox><script setup lang="ts">import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxViewport,} from "@/components/ui/combobox";</script>
<template> <Combobox> <ComboboxInput placeholder="Select a token" show-clear class="w-64" /> <ComboboxList> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxViewport> <ComboboxItem :key="token" :value="token"> {{ token }} </ComboboxItem> </ComboboxViewport> </ComboboxList> </Combobox></template><script lang="ts"> import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "$lib/components/ui/combobox";</script>
<Combobox type="single" bind:value> <ComboboxInput placeholder="Select a token" showClear class="w-64" /> <ComboboxContent> <ComboboxList> {#each filtered as token (token)} <ComboboxItem value={token} label={token} /> {:else} <ComboboxEmpty>No tokens found.</ComboboxEmpty> {/each} </ComboboxList> </ComboboxContent></Combobox>Component source
Section titled “Component source”"use client";
import { Combobox as ComboboxPrimitive } from "@base-ui/react";import { cn } from "cn";import * as React from "react";import { CaretDownIcon, XIcon, CheckIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,} from "@/registry/edmi/ui/input-group";
const Combobox = ComboboxPrimitive.Root;
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;}
function ComboboxTrigger({ className, children, ...props}: ComboboxPrimitive.Trigger.Props) { return ( <ComboboxPrimitive.Trigger data-slot="combobox-trigger" className={cn("[&_svg:not([class*='size-'])]:size-4", className)} {...props} > {children} <CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" /> </ComboboxPrimitive.Trigger> );}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { return ( <ComboboxPrimitive.Clear data-slot="combobox-clear" render={<InputGroupButton variant="ghost" size="icon-xs" />} className={cn(className)} {...props} > <XIcon className="pointer-events-none" /> </ComboboxPrimitive.Clear> );}
function ComboboxInput({ className, children, disabled = false, showTrigger = true, showClear = false, ...props}: ComboboxPrimitive.Input.Props & { showTrigger?: boolean; showClear?: boolean;}) { return ( <InputGroup className={cn("w-auto", className)}> <ComboboxPrimitive.Input render={<InputGroupInput disabled={disabled} />} {...props} /> <InputGroupAddon align="inline-end"> {showTrigger && ( <InputGroupButton size="icon-xs" variant="ghost" render={<ComboboxTrigger />} data-slot="input-group-button" className="group-has-data-[slot=combobox-clear]/input-group:hidden data-[pressed]:bg-transparent" disabled={disabled} /> )} {showClear && <ComboboxClear disabled={disabled} />} </InputGroupAddon> {children} </InputGroup> );}
function ComboboxContent({ className, side = "bottom", sideOffset = 6, align = "start", alignOffset = 0, anchor, ...props}: ComboboxPrimitive.Popup.Props & Pick< ComboboxPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "anchor" >) { return ( <ComboboxPrimitive.Portal> <ComboboxPrimitive.Positioner side={side} sideOffset={sideOffset} align={align} alignOffset={alignOffset} anchor={anchor} className="isolate z-50" > <ComboboxPrimitive.Popup data-slot="combobox-content" data-chips={!!anchor} className={cn( "group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1.5 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:w-auto data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95", className, )} {...props} /> </ComboboxPrimitive.Positioner> </ComboboxPrimitive.Portal> );}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { return ( <ComboboxPrimitive.List data-slot="combobox-list" className={cn( "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1.5 data-[empty]:p-0", className, )} {...props} /> );}
function ComboboxItem({ className, children, ...props}: ComboboxPrimitive.Item.Props) { return ( <ComboboxPrimitive.Item data-slot="combobox-item" className={cn( "relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > {children} <ComboboxPrimitive.ItemIndicator render={ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> } > <CheckIcon className="pointer-events-none" /> </ComboboxPrimitive.ItemIndicator> </ComboboxPrimitive.Item> );}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { return ( <ComboboxPrimitive.Group data-slot="combobox-group" className={cn(className)} {...props} /> );}
function ComboboxLabel({ className, ...props}: ComboboxPrimitive.GroupLabel.Props) { return ( <ComboboxPrimitive.GroupLabel data-slot="combobox-label" className={cn( "px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground", className, )} {...props} /> );}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { return ( <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} /> );}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { return ( <ComboboxPrimitive.Empty data-slot="combobox-empty" className={cn( "hidden w-full justify-center py-2 text-center text-[13.5px] text-muted-foreground group-data-[empty]/combobox-content:flex", className, )} {...props} /> );}
function ComboboxSeparator({ className, ...props}: ComboboxPrimitive.Separator.Props) { return ( <ComboboxPrimitive.Separator data-slot="combobox-separator" className={cn("-mx-1.5 my-1 h-px bg-border", className)} {...props} /> );}
function ComboboxChips({ className, ...props}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) { return ( <ComboboxPrimitive.Chips data-slot="combobox-chips" className={cn( "flex min-h-9 flex-wrap items-center gap-1 rounded-md border border-input bg-card bg-clip-padding px-3 py-1 text-sm focus-within:border-ring focus-within:shadow-ring has-aria-invalid:border-destructive has-aria-invalid:shadow-ring-error has-data-[slot=combobox-chip]:px-1.5", className, )} {...props} /> );}
function ComboboxChip({ className, children, showRemove = true, ...props}: ComboboxPrimitive.Chip.Props & { showRemove?: boolean;}) { return ( <ComboboxPrimitive.Chip data-slot="combobox-chip" className={cn( "flex h-[22px] w-fit items-center justify-center gap-1 rounded-md border border-border bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0", className, )} {...props} > {children} {showRemove && ( <ComboboxPrimitive.ChipRemove render={<Button variant="ghost" size="icon-xs" />} className="-ml-1 size-5 opacity-50 hover:opacity-100" data-slot="combobox-chip-remove" > <XIcon className="pointer-events-none" /> </ComboboxPrimitive.ChipRemove> )} </ComboboxPrimitive.Chip> );}
function ComboboxChipsInput({ className, ...props}: ComboboxPrimitive.Input.Props) { return ( <ComboboxPrimitive.Input data-slot="combobox-chip-input" className={cn( "min-w-16 flex-1 bg-transparent outline-none placeholder:text-muted-foreground", className, )} {...props} /> );}
function useComboboxAnchor() { return React.useRef<HTMLDivElement | null>(null);}
export { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, ComboboxTrigger, ComboboxValue, useComboboxAnchor,};<script setup lang="ts">import type { ComboboxRootEmits, ComboboxRootProps } from "reka-ui"import { ComboboxRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<ComboboxRootProps>()const emits = defineEmits<ComboboxRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)</script>
<template> <ComboboxRoot v-slot="slotProps" data-slot="combobox" v-bind="forwarded" > <slot v-bind="slotProps" /> </ComboboxRoot></template><script setup lang="ts">import type { ComboboxAnchorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxAnchor, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxAnchorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)</script>
<template> <ComboboxAnchor data-slot="combobox-anchor" v-bind="forwarded" :class="cn('w-[200px]', props.class)" > <slot /> </ComboboxAnchor></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { XIcon } from '@lucide/vue'import { cn } from '@/registry/edmi/lib/utils'import { Button } from '@/registry/edmi/ui/button'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] showRemove?: boolean disabled?: boolean}>(), { showRemove: true,})
const emit = defineEmits<{ (e: 'remove'): void }>()</script>
<template> <div data-slot="combobox-chip" :class="cn('flex h-[22px] w-fit items-center justify-center gap-1 rounded-md border border-border bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0', props.class)" > <slot /> <Button v-if="showRemove" variant="ghost" size="icon-xs" type="button" data-slot="combobox-chip-remove" class="-ml-1 size-5 opacity-50 hover:opacity-100" :disabled="disabled" aria-label="Remove" @click="emit('remove')" > <XIcon class="pointer-events-none" /> </Button> </div></template><script setup lang="ts">import type { ComboboxAnchorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxAnchor, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxAnchorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)</script>
<template> <ComboboxAnchor data-slot="combobox-chips" v-bind="forwarded" :class="cn('flex min-h-9 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-card bg-clip-padding px-3 py-1 text-sm focus-within:border-ring focus-within:shadow-ring has-aria-invalid:border-destructive has-aria-invalid:shadow-ring-error has-data-[slot=combobox-chip]:px-1.5', props.class)" > <slot /> </ComboboxAnchor></template><script setup lang="ts">import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxInput, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxInputProps & { class?: HTMLAttributes['class'] }>()const emits = defineEmits<ComboboxInputEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <ComboboxInput data-slot="combobox-chip-input" v-bind="forwarded" :class="cn('min-w-16 flex-1 bg-transparent outline-none placeholder:text-muted-foreground', props.class)" /></template><script setup lang="ts">import type { ComboboxEmptyProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxEmpty } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <ComboboxEmpty data-slot="combobox-empty" v-bind="delegatedProps" :class="cn('flex w-full justify-center py-2 text-center text-[13.5px] text-muted-foreground', props.class)" > <slot /> </ComboboxEmpty></template><script setup lang="ts">import type { ComboboxGroupProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxGroup, ComboboxLabel } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxGroupProps & { class?: HTMLAttributes['class'] heading?: string}>()
const delegatedProps = reactiveOmit(props, 'class', 'heading')</script>
<template> <ComboboxGroup data-slot="combobox-group" v-bind="delegatedProps" :class="cn(props.class)" > <ComboboxLabel v-if="heading" data-slot="combobox-label" class="px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground" > {{ heading }} </ComboboxLabel> <slot /> </ComboboxGroup></template><script setup lang="ts">import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronDownIcon, XIcon } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxAnchor, ComboboxCancel, ComboboxInput, ComboboxTrigger, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import { InputGroup, InputGroupAddon, InputGroupButton } from '@/registry/edmi/ui/input-group'
defineOptions({ inheritAttrs: false,})
const props = withDefaults(defineProps<ComboboxInputProps & { class?: HTMLAttributes['class'] showTrigger?: boolean showClear?: boolean disabled?: boolean}>(), { showTrigger: true, showClear: false, disabled: false,})
const emits = defineEmits<ComboboxInputEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'showTrigger', 'showClear', 'disabled')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <ComboboxAnchor as-child> <InputGroup :class="cn('w-auto', props.class)"> <ComboboxInput v-bind="{ ...$attrs, ...forwarded }" :disabled="disabled" as-child > <input data-slot="input-group-control" class="h-auto min-w-0 flex-1 rounded-none border-0 bg-transparent px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed" > </ComboboxInput> <InputGroupAddon align="inline-end"> <ComboboxTrigger v-if="showTrigger" as-child :disabled="disabled"> <InputGroupButton size="icon-xs" variant="ghost" data-slot="combobox-trigger" class="group-has-data-[slot=combobox-clear]/input-group:hidden data-[state=open]:bg-transparent" > <ChevronDownIcon class="pointer-events-none size-4 text-muted-foreground" /> </InputGroupButton> </ComboboxTrigger> <ComboboxCancel v-if="showClear" as-child :disabled="disabled"> <InputGroupButton size="icon-xs" variant="ghost" data-slot="combobox-clear" > <XIcon class="pointer-events-none" /> </InputGroupButton> </ComboboxCancel> </InputGroupAddon> <slot /> </InputGroup> </ComboboxAnchor></template><script setup lang="ts">import type { ComboboxItemEmits, ComboboxItemProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { CheckIcon } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <ComboboxItem data-slot="combobox-item" v-bind="forwarded" :class="cn(`relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)" > <slot /> <ComboboxItemIndicator as-child> <span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> <slot name="indicator-icon"> <CheckIcon class="pointer-events-none" /> </slot> </span> </ComboboxItemIndicator> </ComboboxItem></template><script setup lang="ts">import type { ComboboxItemIndicatorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxItemIndicator, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxItemIndicatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)</script>
<template> <ComboboxItemIndicator data-slot="combobox-item-indicator" v-bind="forwarded" :class="cn('pointer-events-none absolute right-2 flex size-4 items-center justify-center', props.class)" > <slot /> </ComboboxItemIndicator></template><script setup lang="ts">import type { ComboboxContentEmits, ComboboxContentProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxContent, ComboboxPortal, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
defineOptions({ inheritAttrs: false,})
const props = withDefaults(defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), { position: 'popper', align: 'start', sideOffset: 6,})const emits = defineEmits<ComboboxContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <ComboboxPortal> <ComboboxContent data-slot="combobox-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn('group/combobox-content relative isolate z-50 max-h-(--reka-combobox-content-available-height) w-(--reka-combobox-trigger-width) min-w-(--reka-combobox-trigger-width) origin-(--reka-combobox-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', props.class)" > <slot /> </ComboboxContent> </ComboboxPortal></template><script setup lang="ts">import type { ComboboxSeparatorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxSeparator } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <ComboboxSeparator data-slot="combobox-separator" v-bind="delegatedProps" :class="cn('-mx-1.5 my-1 h-px bg-border', props.class)" > <slot /> </ComboboxSeparator></template><script setup lang="ts">import type { ComboboxTriggerProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronDownIcon } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxTrigger, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)</script>
<template> <ComboboxTrigger data-slot="combobox-trigger" v-bind="forwarded" :class="cn('[&_svg:not([class*=\'size-\'])]:size-4', props.class)" tabindex="0" > <slot /> <ChevronDownIcon class="pointer-events-none size-4 text-muted-foreground" /> </ComboboxTrigger></template><script setup lang="ts">import type { ComboboxViewportProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { ComboboxViewport, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<ComboboxViewportProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)</script>
<template> <ComboboxViewport data-slot="combobox-viewport" v-bind="forwarded" :class="cn('max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto overscroll-contain p-1.5', props.class)" > <slot /> </ComboboxViewport></template>export { ComboboxCancel } from "reka-ui";export { default as Combobox } from "./Combobox.vue";export { default as ComboboxAnchor } from "./ComboboxAnchor.vue";export { default as ComboboxChip } from "./ComboboxChip.vue";export { default as ComboboxChips } from "./ComboboxChips.vue";export { default as ComboboxChipsInput } from "./ComboboxChipsInput.vue";export { default as ComboboxEmpty } from "./ComboboxEmpty.vue";export { default as ComboboxGroup } from "./ComboboxGroup.vue";export { default as ComboboxInput } from "./ComboboxInput.vue";export { default as ComboboxItem } from "./ComboboxItem.vue";export { default as ComboboxItemIndicator } from "./ComboboxItemIndicator.vue";export { default as ComboboxList } from "./ComboboxList.vue";export { default as ComboboxSeparator } from "./ComboboxSeparator.vue";export { default as ComboboxTrigger } from "./ComboboxTrigger.vue";export { default as ComboboxViewport } from "./ComboboxViewport.vue";<script lang="ts"> import XIcon from "phosphor-svelte/lib/X"; import { Button } from "$lib/registry/ui/button/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, showRemove = true, disabled = false, onremove, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { showRemove?: boolean; disabled?: boolean; onremove?: () => void; } = $props();</script>
<div bind:this={ref} data-slot="combobox-chip" class={cn( "flex h-[22px] w-fit items-center justify-center gap-1 rounded-md border border-border bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0", className )} {...restProps}> {@render children?.()} {#if showRemove} <Button variant="ghost" size="icon-xs" class="-ml-1 size-5 opacity-50 hover:opacity-100" data-slot="combobox-chip-remove" aria-label="Remove" {disabled} onclick={onremove} > <XIcon class="pointer-events-none" /> </Button> {/if}</div><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: ComboboxPrimitive.InputProps = $props();</script>
<ComboboxPrimitive.Input bind:ref data-slot="combobox-chip-input" class={cn( "min-w-16 flex-1 bg-transparent outline-none placeholder:text-muted-foreground", className )} {...restProps}/><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
// Bind `ref` and pass it to `<ComboboxContent customAnchor={chipsRef}>` so the list anchors to the chips. let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="combobox-chips" class={cn( "flex min-h-9 flex-wrap items-center gap-1 rounded-md border border-input bg-card bg-clip-padding px-3 py-1 text-sm focus-within:border-ring focus-within:shadow-ring has-aria-invalid:border-destructive has-aria-invalid:shadow-ring-error has-data-[slot=combobox-chip]:px-1.5", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, sideOffset = 6, align = "start", portalProps, children, ...restProps }: ComboboxPrimitive.ContentProps & { portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ComboboxPrimitive.Portal>>; } = $props();</script>
<ComboboxPrimitive.Portal {...portalProps}> <ComboboxPrimitive.Content bind:ref {sideOffset} {align} data-slot="combobox-content" class={cn( "group/combobox-content relative isolate z-50 max-h-(--bits-select-content-available-height) w-(--bits-select-anchor-width) max-w-(--bits-select-content-available-width) min-w-(--bits-select-anchor-width) origin-(--bits-select-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 *:data-[slot=input-group]:m-1.5 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:w-auto", className )} {...restProps} > {@render children?.()} </ComboboxPrimitive.Content></ComboboxPrimitive.Portal><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
// Bits UI does not filter, so render this when your filtered list is empty: // {#if filtered.length === 0}<ComboboxEmpty>No results.</ComboboxEmpty>{/if} let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="combobox-empty" class={cn( "flex w-full justify-center py-2 text-center text-[13.5px] text-muted-foreground", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: ComboboxPrimitive.GroupProps = $props();</script>
<ComboboxPrimitive.Group bind:ref data-slot="combobox-group" class={cn(className)} {...restProps}/><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import XIcon from "phosphor-svelte/lib/X"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { InputGroup, InputGroupAddon, InputGroupButton, } from "$lib/registry/ui/input-group/index.js"; import { getComboboxContext } from "./context.js"; import type { Snippet } from "svelte";
let { ref = $bindable(null), class: className, children, disabled = false, showTrigger = true, showClear = false, ...restProps }: WithoutChildrenOrChild<ComboboxPrimitive.InputProps> & { children?: Snippet; showTrigger?: boolean; showClear?: boolean; } = $props();
const ctx = getComboboxContext();
function clear() { ctx?.clear(); const el = ref as HTMLInputElement | null; if (el) { el.value = ""; el.dispatchEvent(new Event("input", { bubbles: true })); el.focus(); } }</script>
<InputGroup class={cn("w-auto", className)}> <ComboboxPrimitive.Input bind:ref {disabled} {...restProps}> {#snippet child({ props })} <input {...props} {disabled} data-slot="input-group-control" class="h-auto min-w-0 flex-1 rounded-none border-0 bg-transparent px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed" /> {/snippet} </ComboboxPrimitive.Input> <InputGroupAddon align="inline-end"> {#if showTrigger} <ComboboxPrimitive.Trigger {disabled}> {#snippet child({ props })} <InputGroupButton {...props} size="icon-xs" variant="ghost" data-slot="combobox-trigger" class="group-has-data-[slot=combobox-clear]/input-group:hidden data-[state=open]:bg-transparent" {disabled} > <CaretDownIcon class="pointer-events-none size-4 text-muted-foreground" /> </InputGroupButton> {/snippet} </ComboboxPrimitive.Trigger> {/if} {#if showClear} <InputGroupButton size="icon-xs" variant="ghost" data-slot="combobox-clear" aria-label="Clear" {disabled} onclick={clear} > <XIcon class="pointer-events-none" /> </InputGroupButton> {/if} </InputGroupAddon> {@render children?.()}</InputGroup><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import CheckIcon from "phosphor-svelte/lib/Check"; import { cn, type WithoutChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, value, label, children: childrenProp, ...restProps }: WithoutChild<ComboboxPrimitive.ItemProps> = $props();</script>
<ComboboxPrimitive.Item bind:ref {value} {label} data-slot="combobox-item" class={cn( "relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className )} {...restProps}> {#snippet children({ selected, highlighted })} {#if childrenProp} {@render childrenProp({ selected, highlighted })} {:else} {label || value} {/if} {#if selected} <span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> <CheckIcon class="pointer-events-none" /> </span> {/if} {/snippet}</ComboboxPrimitive.Item><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, children, ...restProps }: ComboboxPrimitive.GroupHeadingProps = $props();</script>
<ComboboxPrimitive.GroupHeading bind:ref data-slot="combobox-label" class={cn("px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground", className)} {...restProps}> {@render children?.()}</ComboboxPrimitive.GroupHeading><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, children, ...restProps }: ComboboxPrimitive.ViewportProps = $props();</script>
<ComboboxPrimitive.Viewport bind:ref data-slot="combobox-list" class={cn( "no-scrollbar max-h-72 scroll-py-1 overflow-y-auto overscroll-contain p-1.5", className )} {...restProps}> {@render children?.()}</ComboboxPrimitive.Viewport><script lang="ts"> import { Separator as SeparatorPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: SeparatorPrimitive.RootProps = $props();</script>
<SeparatorPrimitive.Root bind:ref data-slot="combobox-separator" class={cn("-mx-1.5 my-1 h-px bg-border", className)} {...restProps}/><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { ref = $bindable(null), class: className, children, ...restProps }: WithoutChildrenOrChild<ComboboxPrimitive.TriggerProps> & { children?: Snippet; } = $props();</script>
<ComboboxPrimitive.Trigger bind:ref data-slot="combobox-trigger" class={cn("[&_svg:not([class*='size-'])]:size-4", className)} {...restProps}> {@render children?.()} <CaretDownIcon class="pointer-events-none size-4 text-muted-foreground" /></ComboboxPrimitive.Trigger><script lang="ts"> import { Combobox as ComboboxPrimitive } from "bits-ui"; import { setComboboxContext } from "./context.js";
let { open = $bindable(false), value = $bindable(), ...restProps }: ComboboxPrimitive.RootProps = $props();
setComboboxContext({ clear: () => { value = (restProps.type === "multiple" ? [] : "") as never; }, });</script>
<ComboboxPrimitive.Root bind:open bind:value={value as never} {...restProps} />import { getContext, setContext } from "svelte";
const KEY = Symbol("edmi-combobox");
export type ComboboxContext = { /** Clears the selection (and the typed text) of the nearest Combobox root. */ clear: () => void;};
export function setComboboxContext(ctx: ComboboxContext) { return setContext(KEY, ctx);}
export function getComboboxContext() { return getContext<ComboboxContext | undefined>(KEY);}import Root from "./combobox.svelte";import Chip from "./combobox-chip.svelte";import Chips from "./combobox-chips.svelte";import ChipsInput from "./combobox-chips-input.svelte";import Content from "./combobox-content.svelte";import Empty from "./combobox-empty.svelte";import Group from "./combobox-group.svelte";import Input from "./combobox-input.svelte";import Item from "./combobox-item.svelte";import Label from "./combobox-label.svelte";import List from "./combobox-list.svelte";import Separator from "./combobox-separator.svelte";import Trigger from "./combobox-trigger.svelte";
export { Chip, Chip as ComboboxChip, Chips, Chips as ComboboxChips, ChipsInput, ChipsInput as ComboboxChipsInput, Content, Content as ComboboxContent, Empty, Empty as ComboboxEmpty, Group, Group as ComboboxGroup, Input, Input as ComboboxInput, Item, Item as ComboboxItem, Label, Label as ComboboxLabel, List, List as ComboboxList, Root, // Root as Combobox, Separator, Separator as ComboboxSeparator, Trigger, Trigger as ComboboxTrigger,};import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxValue, useComboboxAnchor,} from "@/components/ui/combobox";import { Fragment } from "react";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];
export default function Demo() { const anchor = useComboboxAnchor(); return ( <Combobox multiple items={tokens} defaultValue={["NVDAx", "MSFTx"]}> <ComboboxChips ref={anchor} className="w-72"> <ComboboxValue> {(values: string[]) => ( <Fragment> {values.map((v) => ( <ComboboxChip key={v}>{v}</ComboboxChip> ))} <ComboboxChipsInput placeholder="Add token" /> </Fragment> )} </ComboboxValue> </ComboboxChips> <ComboboxContent anchor={anchor}> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxList> {(item: string) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> );}<script setup lang="ts">import { ref } from "vue";import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxViewport,} from "@/components/ui/combobox";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];const selected = ref<string[]>(["NVDAx", "MSFTx"]);
function remove(token: string) { selected.value = selected.value.filter((v) => v !== token);}</script>
<template> <Combobox v-model="selected" multiple> <ComboboxChips class="w-72"> <ComboboxChip v-for="token in selected" :key="token" @remove="remove(token)"> {{ token }} </ComboboxChip> <ComboboxChipsInput placeholder="Add token" /> </ComboboxChips> <ComboboxList> <ComboboxEmpty>No tokens found.</ComboboxEmpty> <ComboboxViewport> <ComboboxItem v-for="token in tokens" :key="token" :value="token"> {{ token }} </ComboboxItem> </ComboboxViewport> </ComboboxList> </Combobox></template><script lang="ts"> import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, } from "$lib/components/ui/combobox";
const tokens = ["NVDAx", "MSFTx", "AAPLx", "TSLAx", "AMZNx", "METAx"];
let value = $state<string[]>(["NVDAx", "MSFTx"]); let search = $state(""); let anchor = $state<HTMLElement | null>(null); const filtered = $derived( tokens.filter((t) => t.toLowerCase().includes(search.toLowerCase())) );</script>
<Combobox type="multiple" bind:value> <ComboboxChips bind:ref={anchor} class="w-72"> {#each value as token (token)} <ComboboxChip onremove={() => (value = value.filter((v) => v !== token))}> {token} </ComboboxChip> {/each} <ComboboxChipsInput placeholder="Add token" oninput={(e) => (search = e.currentTarget.value)} /> </ComboboxChips> <ComboboxContent customAnchor={anchor}> <ComboboxList> {#each filtered as token (token)} <ComboboxItem value={token} label={token} /> {:else} <ComboboxEmpty>No tokens found.</ComboboxEmpty> {/each} </ComboboxList> </ComboboxContent></Combobox>| Prop | Type | Default |
|---|---|---|
| items | T[] |
none |
| multiple | boolean |
false |
| ComboboxInput showClear | boolean |
false |
| ComboboxInput showTrigger | boolean |
true |
✦ Edmi additions
Section titled “✦ Edmi additions”- Built on Input Group; chips use the secondary badge look.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue (Reka UI) and Svelte (Bits UI) comboboxes are not built on the Base UI
items/renderAPI. ComposeCombobox,ComboboxInput,ComboboxListandComboboxItemparts with the framework’s own model (v-modelin Vue,bind:valuein Svelte) instead of anitemsarray. - Svelte: Bits UI does not filter, and
ComboboxEmptyis manual. Render it yourself when your filtered list is empty.