Skip to content
edmi-ui

Combobox

Searchable select with an input or chips trigger. Base UI Combobox.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/combobox
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/combobox
Terminal window
yarn dlx shadcn@latest add @edmi-ui/combobox
Terminal window
bunx shadcn@latest add @edmi-ui/combobox

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/combobox
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/combobox
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/combobox
Terminal window
bunx shadcn-vue@latest add @edmi-ui/combobox

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/combobox.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/combobox.tsx
"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,
};
registry/ui/combobox/Combobox.vue
<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>
registry/ui/combobox/ComboboxAnchor.vue
<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>
registry/ui/combobox/ComboboxChip.vue
<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>
registry/ui/combobox/ComboboxChips.vue
<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>
registry/ui/combobox/ComboboxChipsInput.vue
<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>
registry/ui/combobox/ComboboxEmpty.vue
<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>
registry/ui/combobox/ComboboxGroup.vue
<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>
registry/ui/combobox/ComboboxInput.vue
<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>
registry/ui/combobox/ComboboxItem.vue
<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>
registry/ui/combobox/ComboboxItemIndicator.vue
<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>
registry/ui/combobox/ComboboxList.vue
<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>
registry/ui/combobox/ComboboxSeparator.vue
<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>
registry/ui/combobox/ComboboxTrigger.vue
<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>
registry/ui/combobox/ComboboxViewport.vue
<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>
registry/ui/combobox/index.ts
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";
src/lib/registry/ui/combobox/combobox-chip.svelte
<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>
src/lib/registry/ui/combobox/combobox-chips-input.svelte
<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}
/>
src/lib/registry/ui/combobox/combobox-chips.svelte
<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>
src/lib/registry/ui/combobox/combobox-content.svelte
<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>
src/lib/registry/ui/combobox/combobox-empty.svelte
<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>
src/lib/registry/ui/combobox/combobox-group.svelte
<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}
/>
src/lib/registry/ui/combobox/combobox-input.svelte
<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>
src/lib/registry/ui/combobox/combobox-item.svelte
<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>
src/lib/registry/ui/combobox/combobox-label.svelte
<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>
src/lib/registry/ui/combobox/combobox-list.svelte
<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>
src/lib/registry/ui/combobox/combobox-separator.svelte
<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}
/>
src/lib/registry/ui/combobox/combobox-trigger.svelte
<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>
src/lib/registry/ui/combobox/combobox.svelte
<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} />
src/lib/registry/ui/combobox/context.ts
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);
}
src/lib/registry/ui/combobox/index.ts
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,
};
StackBlitz ↗StackBlitz ↗StackBlitz ↗
NVDAx
MSFTx
NVDAx
MSFTx
Prop Type Default
items T[] none
multiple boolean false
ComboboxInput showClear boolean false
ComboboxInput showTrigger boolean true
  • Built on Input Group; chips use the secondary badge look.
  • Vue (Reka UI) and Svelte (Bits UI) comboboxes are not built on the Base UI items / render API. Compose Combobox, ComboboxInput, ComboboxList and ComboboxItem parts with the framework’s own model (v-model in Vue, bind:value in Svelte) instead of an items array.
  • Svelte: Bits UI does not filter, and ComboboxEmpty is manual. Render it yourself when your filtered list is empty.