Combobox
Autocomplete input with a list of suggestions.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxDemo() {
return () => (
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/combobox.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/combobox.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/combobox.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/combobox.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, on, ref, type Handle, type MixValue, type Props, type RemixNode } from 'remix/component'
import { Button } from './button.tsx'
import { CheckIcon, ChevronDownIcon, XIcon } from './icons.tsx'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './input-group.tsx'
import { floatingLayer, type Align, type Side } from './popover.tsx'
// ---------------------------------------------------------------------------------------------
// Combobox (Base UI Combobox semantics, which shadcn's combobox is built on)
// ---------------------------------------------------------------------------------------------
/** A group of items: `{ value: 'Europe', items: [...] }`, rendered with `ComboboxGroup`. */
export interface ComboboxItemGroup<Item = unknown> {
value: string
items: readonly Item[]
}
interface ComboboxContext {
readonly open: boolean
readonly multiple: boolean
readonly disabled: boolean
readonly autoHighlight: boolean
readonly selected: unknown[]
/** Text typed into the input. */
readonly query: string
/** Items (or groups with their matching items) left after filtering by `query`. */
readonly filtered: readonly unknown[]
readonly empty: boolean
readonly listId: string
highlighted: unknown
input: HTMLInputElement | null
/** Element the popup is positioned against (input, chips container, or trigger). */
anchor: HTMLElement | null
trigger: HTMLElement | null
content: HTMLElement | null
/** Rendered item elements in DOM order, mapped to their values (for keyboard navigation). */
elements: Map<HTMLElement, unknown>
inputInsidePopup: boolean
label(item: unknown): string
isSelected(item: unknown): boolean
setOpen(open: boolean): void
setQuery(query: string): void
toggle(item: unknown): void
remove(item: unknown): void
clear(): void
highlight(item: unknown): void
move(delta: 1 | -1 | 'first' | 'last'): void
}
/**
* A callback typed by the caller, e.g. `(item: Country) => …`. The combobox only ever passes its
* own `items` (and their groups) back into these, so the caller's item type holds; `never` lets any
* parameter type be assigned.
*/
export type ItemCallback<Result, Extra extends unknown[] = []> = (item: never, ...extra: Extra) => Result
function callWith<Result, Extra extends unknown[]>(
callback: ItemCallback<Result, Extra>,
item: unknown,
...extra: Extra
): Result {
// Sound by construction: `item` always comes from the caller's own `items`.
let typed = callback as (item: unknown, ...extra: Extra) => Result
return typed(item, ...extra)
}
export interface ComboboxProps {
children?: RemixNode
items?: readonly unknown[]
multiple?: boolean
value?: unknown
defaultValue?: unknown
onValueChange?: ItemCallback<void>
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
inputValue?: string
defaultInputValue?: string
onInputValueChange?: (value: string) => void
/** Highlights the first matching item while typing. */
autoHighlight?: boolean
disabled?: boolean
/** Text shown in the input / `ComboboxValue` for an item. */
itemToStringLabel?: ItemCallback<string>
/** String submitted with forms; also the label when `itemToStringLabel` is not set. */
itemToStringValue?: ItemCallback<string>
/** Custom filter. Defaults to a case-insensitive "contains" match on the item label. */
filter?: ItemCallback<boolean, [query: string]>
name?: string
}
const isGroup = (item: unknown): item is ComboboxItemGroup =>
typeof item === 'object' && item !== null && 'items' in item && Array.isArray(item.items)
const toArray = (value: unknown): unknown[] =>
value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]
export function Combobox(handle: Handle<ComboboxProps, ComboboxContext>) {
let uncontrolledValue = toArray(handle.props.defaultValue)
let uncontrolledOpen = handle.props.defaultOpen ?? false
let uncontrolledQuery = handle.props.defaultInputValue
let filtering = false
let label = (item: unknown): string => {
if (item === undefined || item === null) return ''
let { itemToStringLabel, itemToStringValue } = handle.props
if (itemToStringLabel) return callWith(itemToStringLabel, item)
if (itemToStringValue) return callWith(itemToStringValue, item)
if (typeof item === 'object' && 'label' in item) return String(item.label)
return String(item)
}
let equal = (a: unknown, b: unknown) => a === b || (a !== undefined && b !== undefined && label(a) === label(b))
let commit = (next: unknown[]) => {
if (handle.props.value === undefined) uncontrolledValue = next
let { onValueChange } = handle.props
if (onValueChange) callWith(onValueChange, context.multiple ? next : (next[0] ?? null))
}
let context: ComboboxContext = {
get open() {
return handle.props.open ?? uncontrolledOpen
},
get multiple() {
return handle.props.multiple ?? false
},
get disabled() {
return handle.props.disabled ?? false
},
get autoHighlight() {
return handle.props.autoHighlight ?? false
},
get selected() {
return handle.props.value === undefined ? uncontrolledValue : toArray(handle.props.value)
},
get query() {
if (handle.props.inputValue !== undefined) return handle.props.inputValue
if (uncontrolledQuery !== undefined) return uncontrolledQuery
// A single-select input shows the selected item's label until the user types.
return context.multiple || context.inputInsidePopup ? '' : label(context.selected[0])
},
get filtered() {
let items = handle.props.items ?? []
let query = filtering ? context.query.trim().toLowerCase() : ''
if (!query) return items
let { filter } = handle.props
let matches = (item: unknown) =>
filter ? callWith(filter, item, query) : label(item).toLowerCase().includes(query)
if (items.some(isGroup)) {
return items
.filter(isGroup)
.map((group) => ({ ...group, items: group.items.filter(matches) }))
.filter((group) => group.items.length > 0)
}
return items.filter(matches)
},
get empty() {
return context.filtered.length === 0
},
listId: `${handle.id}-list`,
highlighted: undefined,
input: null,
anchor: null,
trigger: null,
content: null,
elements: new Map(),
inputInsidePopup: false,
label,
isSelected: (item) => context.selected.some((selected) => equal(selected, item)),
setOpen(open) {
if (open === context.open || (open && context.disabled)) return
if (!open) {
filtering = false
context.highlighted = undefined
// Abandoned typing reverts to the selected label (single) or clears (multiple/popup).
if (handle.props.inputValue === undefined) uncontrolledQuery = undefined
}
if (handle.props.open === undefined) uncontrolledOpen = open
handle.props.onOpenChange?.(open)
void handle.update()
},
setQuery(query) {
filtering = true
if (handle.props.inputValue === undefined) uncontrolledQuery = query
handle.props.onInputValueChange?.(query)
if (!context.open) context.setOpen(true)
context.highlighted = undefined
void handle.update()
// Base UI's autoHighlight: keep the first match highlighted while typing.
if (context.autoHighlight) handle.queueTask(() => context.move('first'))
},
toggle(item) {
if (context.multiple) {
let next = context.isSelected(item)
? context.selected.filter((selected) => !equal(selected, item))
: [...context.selected, item]
commit(next)
filtering = false
if (handle.props.inputValue === undefined) uncontrolledQuery = undefined
void handle.update()
return
}
commit([item])
context.setOpen(false)
void handle.update()
},
remove(item) {
commit(context.selected.filter((selected) => !equal(selected, item)))
void handle.update()
},
clear() {
commit([])
filtering = false
if (handle.props.inputValue === undefined) uncontrolledQuery = undefined
context.input?.focus()
void handle.update()
},
highlight(item) {
if (equal(context.highlighted, item) && context.highlighted !== undefined) return
context.highlighted = item
void handle.update()
},
move(delta) {
let entries = [...context.elements.entries()]
.filter(([element]) => element.isConnected && !element.hasAttribute('data-disabled'))
.sort(([a], [b]) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
if (entries.length === 0) return context.highlight(undefined)
let index = entries.findIndex(([, value]) => context.highlighted !== undefined && equal(value, context.highlighted))
let next =
delta === 'first'
? 0
: delta === 'last'
? entries.length - 1
: index === -1
? delta === 1
? 0
: entries.length - 1
: (index + delta + entries.length) % entries.length
let [element, value] = entries[next]!
context.highlight(value)
element.scrollIntoView({ block: 'nearest' })
},
}
handle.context.set(context)
return () => {
let { children, name } = handle.props
let { itemToStringValue } = handle.props
return (
<>
{children}
{name !== undefined
? context.selected.map((item, index) => (
<input
key={index}
type="hidden"
name={name}
value={itemToStringValue ? callWith(itemToStringValue, item) : label(item)}
/>
))
: null}
</>
)
}
}
// ---------------------------------------------------------------------------------------------
// Input & triggers
// ---------------------------------------------------------------------------------------------
/** Keyboard handling shared by `ComboboxInput` and `ComboboxChipsInput`. */
function inputBehavior(context: ComboboxContext) {
return [
ref<HTMLInputElement>((node, signal) => {
context.input = node
context.inputInsidePopup = !!node.closest('[data-slot="combobox-content"]')
if (!context.inputInsidePopup && !context.anchor) context.anchor = node
signal.addEventListener('abort', () => {
if (context.input === node) context.input = null
if (context.anchor === node) context.anchor = null
})
}),
on<HTMLInputElement, 'input'>('input', (event) => context.setQuery(event.currentTarget.value)),
on<HTMLInputElement, 'click'>('click', () => {
if (!context.inputInsidePopup && context.multiple) context.setOpen(true)
}),
on<HTMLInputElement, 'keydown'>('keydown', (event) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!context.open) {
context.setOpen(true)
return
}
context.move(event.key === 'ArrowDown' ? 1 : -1)
} else if ((event.key === 'Home' || event.key === 'End') && context.open && event.ctrlKey) {
event.preventDefault()
context.move(event.key === 'Home' ? 'first' : 'last')
} else if (event.key === 'Enter' && context.open) {
event.preventDefault()
if (context.highlighted !== undefined) context.toggle(context.highlighted)
} else if (event.key === 'Tab' && context.open && !context.inputInsidePopup) {
context.setOpen(false)
} else if (event.key === 'Backspace' && context.multiple && event.currentTarget.value === '') {
let last = context.selected[context.selected.length - 1]
if (last !== undefined) context.remove(last)
}
}),
]
}
export type ComboboxInputProps = Omit<Props<'input'>, 'mix' | 'type' | 'role' | 'list'> & {
type?: 'text'
/** Applied to the wrapping `InputGroup`, like shadcn's `className`. */
mix?: MixValue<HTMLElement>
showTrigger?: boolean
showClear?: boolean
}
const inputGroupStyles = css({ width: 'auto' })
const inputTriggerStyles = css({
'[data-slot="input-group"]:has([data-slot="combobox-clear"]) &': { display: 'none' },
'&[data-pressed]': { backgroundColor: 'transparent' },
})
export function ComboboxInput(handle: Handle<ComboboxInputProps>) {
let context = handle.context.get(Combobox)
let behavior = inputBehavior(context)
return () => {
let { showTrigger = true, showClear = false, disabled = false, children, mix, ...props } = handle.props
let hasValue = context.selected.length > 0
return (
<InputGroup mix={[inputGroupStyles, mix]}>
<InputGroupInput
role="combobox"
aria-autocomplete="list"
aria-expanded={context.open ? 'true' : 'false'}
aria-controls={context.listId}
autoComplete="off"
disabled={disabled || context.disabled}
value={context.query}
{...props}
mix={behavior}
/>
<InputGroupAddon align="inline-end">
{showTrigger ? (
<ComboboxTrigger
data-slot="input-group-button"
disabled={disabled || context.disabled}
asInputGroupButton
mix={inputTriggerStyles}
/>
) : null}
{showClear && hasValue ? <ComboboxClear disabled={disabled || context.disabled} /> : null}
</InputGroupAddon>
{children}
</InputGroup>
)
}
}
export interface ComboboxTriggerProps extends Props<'button'> {
/** Styles the trigger as the ghost `icon-xs` input-group button (used inside `ComboboxInput`). */
asInputGroupButton?: boolean
}
const triggerStyles = css({ '& svg:not([class*="size-"])': { width: '1rem', height: '1rem' } })
const triggerIconStyles = css({ pointerEvents: 'none', width: '1rem', height: '1rem', color: 'var(--muted-foreground)' })
/**
* Toggles the popup. Without `ComboboxInput` it is the anchor too, e.g. a button-styled trigger:
* `<ComboboxTrigger mix={buttonVariants({ variant: 'outline' })}><ComboboxValue /></ComboboxTrigger>`.
*/
export function ComboboxTrigger(handle: Handle<ComboboxTriggerProps>) {
let context = handle.context.get(Combobox)
let behavior = [
ref<HTMLButtonElement>((node, signal) => {
context.trigger = node
signal.addEventListener('abort', () => {
if (context.trigger === node) context.trigger = null
})
}),
on<HTMLButtonElement, 'click'>('click', () => {
context.setOpen(!context.open)
if (!context.inputInsidePopup) context.input?.focus()
}),
on<HTMLButtonElement, 'keydown'>('keydown', (event) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
context.setOpen(true)
}
}),
]
return () => {
let { asInputGroupButton, children, mix, ...props } = handle.props
let shared = {
'data-slot': 'combobox-trigger',
'aria-haspopup': 'listbox' as const,
'aria-expanded': context.open ? ('true' as const) : ('false' as const),
'data-pressed': context.open ? '' : undefined,
tabIndex: asInputGroupButton ? -1 : undefined,
}
let content = (
<>
{children}
<ChevronDownIcon data-slot="combobox-trigger-icon" mix={triggerIconStyles} />
</>
)
if (asInputGroupButton) {
return (
<InputGroupButton size="icon-xs" variant="ghost" {...shared} {...props} mix={[triggerStyles, behavior, mix]}>
{content}
</InputGroupButton>
)
}
return (
<button type="button" {...shared} {...props} mix={[triggerStyles, behavior, mix]}>
{content}
</button>
)
}
}
export function ComboboxClear(handle: Handle<Props<'button'>>) {
let context = handle.context.get(Combobox)
return () => {
let { mix, ...props } = handle.props
return (
<InputGroupButton
data-slot="combobox-clear"
variant="ghost"
size="icon-xs"
aria-label="Clear"
{...props}
mix={[on('click', () => context.clear()), mix]}
>
<XIcon mix={css({ pointerEvents: 'none' })} />
</InputGroupButton>
)
}
}
export interface ComboboxValueProps {
/** Render function receiving the selected value (single) or values (multiple). */
children?: RemixNode | ItemCallback<RemixNode>
placeholder?: RemixNode
}
export function ComboboxValue(handle: Handle<ComboboxValueProps>) {
let context = handle.context.get(Combobox)
return () => {
let { children, placeholder } = handle.props
let value = context.multiple ? context.selected : (context.selected[0] ?? null)
if (typeof children === 'function') return callWith(children, value)
if (children !== undefined) return children
return context.selected.length === 0 ? (placeholder ?? null) : context.selected.map(context.label).join(', ')
}
}
// ---------------------------------------------------------------------------------------------
// Popup
// ---------------------------------------------------------------------------------------------
export interface ComboboxContentProps extends Props<'div'> {
side?: Side
align?: Align
sideOffset?: number
alignOffset?: number
}
const contentStyles = css({
position: 'relative',
zIndex: '50',
maxHeight: '24rem',
width: 'var(--radix-combobox-trigger-width)',
maxWidth: 'var(--radix-combobox-content-available-width)',
minWidth: 'calc(var(--radix-combobox-trigger-width) + 1.75rem)',
transformOrigin: 'var(--radix-combobox-content-transform-origin)',
overflow: 'hidden',
borderRadius: 'calc(var(--radius) - 2px)',
backgroundColor: 'var(--popover)',
color: 'var(--popover-foreground)',
'--tw-shadow': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
'--tw-ring-shadow': '0 0 0 1px color-mix(in oklab, var(--foreground) 10%, transparent)',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
'&[data-chips="true"]': { minWidth: 'var(--radix-combobox-trigger-width)' },
'&[data-side="bottom"]': { '--tw-enter-translate-y': '-0.5rem', '--tw-exit-translate-y': '-0.5rem' },
'&[data-side="left"]': { '--tw-enter-translate-x': '0.5rem', '--tw-exit-translate-x': '0.5rem' },
'&[data-side="right"]': { '--tw-enter-translate-x': '-0.5rem', '--tw-exit-translate-x': '-0.5rem' },
'&[data-side="top"]': { '--tw-enter-translate-y': '0.5rem', '--tw-exit-translate-y': '0.5rem' },
'& > [data-slot="input-group"]': {
margin: '0.25rem',
marginBottom: 0,
height: '2rem',
borderColor: 'color-mix(in oklab, var(--input) 30%, transparent)',
backgroundColor: 'color-mix(in oklab, var(--input) 30%, transparent)',
'--tw-shadow': '0 0 #0000',
},
'&[data-state="open"]': {
animation: 'enter 100ms ease',
'--tw-enter-opacity': '0',
'--tw-enter-scale': '0.95',
},
'&[data-state="closed"]': {
animation: 'exit 100ms ease forwards',
'--tw-exit-opacity': '0',
'--tw-exit-scale': '0.95',
},
})
export function ComboboxContent(handle: Handle<ComboboxContentProps>) {
let context = handle.context.get(Combobox)
let wasOpen = false
return () => {
let { side = 'bottom', align = 'start', sideOffset = 6, alignOffset = 0, mix, ...props } = handle.props
let chips = context.anchor?.dataset.slot === 'combobox-chips'
if (context.open && !wasOpen && context.inputInsidePopup) {
handle.queueTask(() => requestAnimationFrame(() => context.input?.focus()))
}
wasOpen = context.open
return (
<div
data-slot="combobox-content"
data-chips={chips ? 'true' : 'false'}
data-state={context.open ? 'open' : 'closed'}
data-open={context.open ? '' : undefined}
data-closed={context.open ? undefined : ''}
data-empty={context.empty ? '' : undefined}
{...props}
mix={[
contentStyles,
ref((node, signal) => {
context.content = node
signal.addEventListener('abort', () => {
if (context.content === node) context.content = null
})
}),
floatingLayer({
open: context.open,
anchor: () => context.anchor ?? context.trigger,
name: 'combobox',
side,
align,
sideOffset,
alignOffset,
inside: (target) =>
!!(context.anchor?.contains(target) || context.trigger?.contains(target) ||
(target instanceof Element && target.closest('[data-slot="input-group"]')?.contains(context.input))),
onDismiss: () => context.setOpen(false),
onClose: () => {
if (context.inputInsidePopup) context.trigger?.focus()
},
}),
mix,
]}
/>
)
}
}
const listStyles = css({
maxHeight: 'min(calc(24rem - 2.25rem), calc(var(--radix-combobox-content-available-height) - 2.25rem))',
scrollPaddingBlock: '0.25rem',
overflowY: 'auto',
padding: '0.25rem',
'&[data-empty]': { padding: 0 },
})
export interface ComboboxListProps extends Omit<Props<'div'>, 'children'> {
children?: RemixNode | ItemCallback<RemixNode, [index: number]>
}
export function ComboboxList(handle: Handle<ComboboxListProps>) {
let context = handle.context.get(Combobox)
return () => {
let { children, mix, ...props } = handle.props
return (
<div
data-slot="combobox-list"
id={context.listId}
role="listbox"
aria-multiselectable={context.multiple ? 'true' : undefined}
data-empty={context.empty ? '' : undefined}
{...props}
mix={[listStyles, mix]}
>
{typeof children === 'function'
? context.filtered.map((item, index) => callWith(children, item, index))
: children}
</div>
)
}
}
// ---------------------------------------------------------------------------------------------
// Items, groups, collections
// ---------------------------------------------------------------------------------------------
export interface ComboboxItemProps extends Props<'div'> {
value: unknown
disabled?: boolean
}
const itemStyles = css({
position: 'relative',
display: 'flex',
width: '100%',
cursor: 'default',
alignItems: 'center',
gap: '0.5rem',
borderRadius: 'calc(var(--radius) - 4px)',
paddingBlock: '0.375rem',
paddingRight: '2rem',
paddingLeft: '0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
outline: '2px solid transparent',
outlineOffset: '2px',
userSelect: 'none',
'&[data-highlighted]': { backgroundColor: 'var(--accent)', color: 'var(--accent-foreground)' },
'&[data-disabled]': { pointerEvents: 'none', opacity: 0.5 },
'& svg': { pointerEvents: 'none', flexShrink: 0 },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
})
const indicatorStyles = css({
pointerEvents: 'none',
position: 'absolute',
right: '0.5rem',
display: 'flex',
width: '1rem',
height: '1rem',
alignItems: 'center',
justifyContent: 'center',
})
const checkStyles = css({
pointerEvents: 'none',
width: '1rem',
height: '1rem',
'@media (pointer: coarse)': { width: '1.25rem', height: '1.25rem' },
})
export function ComboboxItem(handle: Handle<ComboboxItemProps>) {
let context = handle.context.get(Combobox)
let behavior = [
ref<HTMLDivElement>((node, signal) => {
context.elements.set(node, handle.props.value)
signal.addEventListener('abort', () => context.elements.delete(node))
}),
on<HTMLDivElement, 'pointermove'>('pointermove', () => {
if (!handle.props.disabled) context.highlight(handle.props.value)
}),
// Keep focus in the input while picking with the pointer.
on<HTMLDivElement, 'pointerdown'>('pointerdown', (event) => event.preventDefault()),
on<HTMLDivElement, 'click'>('click', () => {
if (!handle.props.disabled) context.toggle(handle.props.value)
}),
]
return () => {
let { value, disabled, children, mix, ...props } = handle.props
let selected = context.isSelected(value)
let highlighted =
context.highlighted !== undefined && context.label(context.highlighted) === context.label(value)
let id = `${context.listId}-${context.label(value).replace(/\s+/g, '-')}`
if (highlighted && context.input) context.input.setAttribute('aria-activedescendant', id)
return (
<div
data-slot="combobox-item"
id={id}
role="option"
aria-selected={selected ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : undefined}
data-selected={selected ? '' : undefined}
data-highlighted={highlighted ? '' : undefined}
data-disabled={disabled ? '' : undefined}
{...props}
mix={[itemStyles, behavior, mix]}
>
{children}
{selected ? (
<span data-slot="combobox-item-indicator" mix={indicatorStyles}>
<CheckIcon mix={checkStyles} />
</span>
) : null}
</div>
)
}
}
interface ComboboxGroupContext {
items: readonly unknown[]
}
export interface ComboboxGroupProps extends Props<'div'> {
/** The group's (already filtered) items, rendered by `ComboboxCollection`. */
items?: readonly unknown[]
}
export function ComboboxGroup(handle: Handle<ComboboxGroupProps, ComboboxGroupContext>) {
let group: ComboboxGroupContext = { items: [] }
handle.context.set(group)
return () => {
let { items = [], ...props } = handle.props
group.items = items
return <div data-slot="combobox-group" role="group" {...props} />
}
}
export function ComboboxCollection(handle: Handle<{ children?: ItemCallback<RemixNode, [index: number]> }>) {
let group = handle.context.get(ComboboxGroup)
return () => {
let { children } = handle.props
return children ? group.items.map((item, index) => callWith(children, item, index)) : null
}
}
const labelStyles = css({
paddingInline: '0.5rem',
paddingBlock: '0.375rem',
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
color: 'var(--muted-foreground)',
'@media (pointer: coarse)': {
paddingInline: '0.75rem',
paddingBlock: '0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
},
})
export function ComboboxLabel(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="combobox-label" {...props} mix={[labelStyles, mix]} />
}
}
const emptyStyles = css({
display: 'none',
width: '100%',
justifyContent: 'center',
paddingBlock: '0.5rem',
textAlign: 'center',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
'[data-slot="combobox-content"][data-empty] &': { display: 'flex' },
})
export function ComboboxEmpty(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="combobox-empty" role="status" {...props} mix={[emptyStyles, mix]} />
}
}
const separatorStyles = css({
marginInline: '-0.25rem',
marginBlock: '0.25rem',
height: '1px',
backgroundColor: 'var(--border)',
})
export function ComboboxSeparator(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="combobox-separator" role="separator" {...props} mix={[separatorStyles, mix]} />
}
}
// ---------------------------------------------------------------------------------------------
// Chips (multiple selection)
// ---------------------------------------------------------------------------------------------
const chipsStyles = css({
display: 'flex',
minHeight: '2.25rem',
flexWrap: 'wrap',
alignItems: 'center',
gap: '0.375rem',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
borderColor: 'var(--input)',
backgroundColor: 'transparent',
backgroundClip: 'padding-box',
paddingInline: '0.625rem',
paddingBlock: '0.375rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
'--tw-shadow': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
transitionProperty: 'color, box-shadow',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
'&:focus-within': {
borderColor: 'var(--ring)',
'--tw-ring-shadow': '0 0 0 3px color-mix(in oklab, var(--ring) 50%, transparent)',
},
'&:has([aria-invalid="true"])': {
borderColor: 'var(--destructive)',
'--tw-ring-shadow': '0 0 0 3px color-mix(in oklab, var(--destructive) 20%, transparent)',
},
'&:has([data-slot="combobox-chip"])': { paddingInline: '0.375rem' },
'.dark &': { backgroundColor: 'color-mix(in oklab, var(--input) 30%, transparent)' },
'.dark &:has([aria-invalid="true"])': {
borderColor: 'color-mix(in oklab, var(--destructive) 50%, transparent)',
'--tw-ring-shadow': '0 0 0 3px color-mix(in oklab, var(--destructive) 40%, transparent)',
},
})
/** The chips container; it is also the popup's anchor (shadcn's `useComboboxAnchor()`). */
export function ComboboxChips(handle: Handle<Props<'div'>>) {
let context = handle.context.get(Combobox)
return () => {
let { mix, ...props } = handle.props
return (
<div
data-slot="combobox-chips"
{...props}
mix={[
chipsStyles,
ref((node, signal) => {
context.anchor = node
signal.addEventListener('abort', () => {
if (context.anchor === node) context.anchor = null
})
}),
on<HTMLDivElement, 'pointerdown'>('pointerdown', (event) => {
if (event.target === event.currentTarget) {
event.preventDefault()
context.input?.focus()
context.setOpen(true)
}
}),
mix,
]}
/>
)
}
}
export interface ComboboxChipProps extends Props<'div'> {
/** The selected item this chip represents. Defaults to the item whose label is the chip text. */
value?: unknown
showRemove?: boolean
}
const chipStyles = css({
display: 'flex',
height: '1.375rem',
width: 'fit-content',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
borderRadius: 'calc(var(--radius) - 4px)',
backgroundColor: 'var(--muted)',
paddingInline: '0.375rem',
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
fontWeight: 500,
whiteSpace: 'nowrap',
color: 'var(--foreground)',
'&:has(:disabled)': { pointerEvents: 'none', cursor: 'not-allowed', opacity: 0.5 },
'&:has([data-slot="combobox-chip-remove"])': { paddingRight: 0 },
})
const chipRemoveStyles = css({
marginLeft: '-0.25rem',
opacity: 0.5,
'&:hover': { opacity: 1 },
})
export function ComboboxChip(handle: Handle<ComboboxChipProps>) {
let context = handle.context.get(Combobox)
return () => {
let { value, showRemove = true, children, mix, ...props } = handle.props
let item =
value ?? context.selected.find((selected) => context.label(selected) === String(children ?? ''))
return (
<div data-slot="combobox-chip" {...props} mix={[chipStyles, mix]}>
{children}
{showRemove ? (
<Button
data-slot="combobox-chip-remove"
variant="ghost"
size="icon-xs"
tabIndex={-1}
aria-label={`Remove ${context.label(item)}`}
disabled={context.disabled}
mix={[chipRemoveStyles, on('click', () => {
if (item !== undefined) context.remove(item)
})]}
>
<XIcon mix={css({ pointerEvents: 'none' })} />
</Button>
) : null}
</div>
)
}
}
const chipsInputStyles = css({ minWidth: '4rem', flex: '1 1 0%', outlineStyle: 'none' })
export type ComboboxChipsInputProps = Omit<Props<'input'>, 'type' | 'role' | 'list'>
export function ComboboxChipsInput(handle: Handle<ComboboxChipsInputProps>) {
let context = handle.context.get(Combobox)
let behavior = inputBehavior(context)
return () => {
let { mix, ...props } = handle.props
return (
<input
data-slot="combobox-chip-input"
role="combobox"
aria-autocomplete="list"
aria-expanded={context.open ? 'true' : 'false'}
aria-controls={context.listId}
autoComplete="off"
disabled={context.disabled}
value={context.query}
{...props}
mix={[chipsInputStyles, behavior, mix]}
/>
)
}
}Update the import paths to match your project setup.
Usage
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"]
export function ExampleCombobox() {
return (
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Examples
Multiple
A combobox with multiple selection using multiple and ComboboxChips.
import { css } from 'remix/component'
import {
Combobox,
ComboboxChip,
ComboboxChips,import { css } from 'remix/component'
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxMultiple() {
return () => (
<Combobox multiple autoHighlight items={frameworks} defaultValue={[frameworks[0]]}>
<ComboboxChips mix={css({ width: '100%', maxWidth: '20rem' })}>
<ComboboxValue>
{(values: string[]) => (
<>
{values.map((value) => (
<ComboboxChip key={value}>{value}</ComboboxChip>
))}
<ComboboxChipsInput />
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Clear Button
Use the showClear prop to show a clear button.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxClear() {
return () => (
<Combobox items={frameworks} defaultValue={frameworks[0]}>
<ComboboxInput placeholder="Select a framework" showClear />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Groups
Use ComboboxGroup and ComboboxSeparator to group items.
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxSeparator,
} from '#app/ui/combobox.tsx'
const timezones = [
{
value: 'Americas',
items: [
'(GMT-5) New York',
'(GMT-8) Los Angeles',
'(GMT-6) Chicago',
'(GMT-5) Toronto',
'(GMT-8) Vancouver',
'(GMT-3) São Paulo',
],
},
{
value: 'Europe',
items: [
'(GMT+0) London',
'(GMT+1) Paris',
'(GMT+1) Berlin',
'(GMT+1) Rome',
'(GMT+1) Madrid',
'(GMT+1) Amsterdam',
],
},
{
value: 'Asia/Pacific',
items: [
'(GMT+9) Tokyo',
'(GMT+8) Shanghai',
'(GMT+8) Singapore',
'(GMT+4) Dubai',
'(GMT+11) Sydney',
'(GMT+9) Seoul',
],
},
] as const
type TimezoneGroup = (typeof timezones)[number]
export function ComboboxGroups() {
return () => (
<Combobox items={timezones}>
<ComboboxInput placeholder="Select a timezone" />
<ComboboxContent>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group: TimezoneGroup, index: number) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
{index < timezones.length - 1 && <ComboboxSeparator />}
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Custom Items
You can render a custom component inside ComboboxItem.
South America (ar)
Oceania (au)
South America (br)
North America (ca)
Asia (cn)
South America (co)
Africa (eg)
Europe (fr)
Europe (de)
Europe (it)
Asia (jp)
Africa (ke)
North America (mx)
Oceania (nz)
Africa (ng)
Africa (za)
Asia (kr)
Europe (gb)
North America (us)
import { css } from 'remix/component'
import {
Combobox,
ComboboxContent,
ComboboxEmpty,import { css } from 'remix/component'
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
import { Item, ItemContent, ItemDescription, ItemTitle } from '#app/ui/item.tsx'
const countries = [
{ code: '', value: '', continent: '', label: 'Select country' },
{
code: 'ar',
value: 'argentina',
label: 'Argentina',
continent: 'South America',
},
{ code: 'au', value: 'australia', label: 'Australia', continent: 'Oceania' },
{ code: 'br', value: 'brazil', label: 'Brazil', continent: 'South America' },
{ code: 'ca', value: 'canada', label: 'Canada', continent: 'North America' },
{ code: 'cn', value: 'china', label: 'China', continent: 'Asia' },
{
code: 'co',
value: 'colombia',
label: 'Colombia',
continent: 'South America',
},
{ code: 'eg', value: 'egypt', label: 'Egypt', continent: 'Africa' },
{ code: 'fr', value: 'france', label: 'France', continent: 'Europe' },
{ code: 'de', value: 'germany', label: 'Germany', continent: 'Europe' },
{ code: 'it', value: 'italy', label: 'Italy', continent: 'Europe' },
{ code: 'jp', value: 'japan', label: 'Japan', continent: 'Asia' },
{ code: 'ke', value: 'kenya', label: 'Kenya', continent: 'Africa' },
{ code: 'mx', value: 'mexico', label: 'Mexico', continent: 'North America' },
{
code: 'nz',
value: 'new-zealand',
label: 'New Zealand',
continent: 'Oceania',
},
{ code: 'ng', value: 'nigeria', label: 'Nigeria', continent: 'Africa' },
{
code: 'za',
value: 'south-africa',
label: 'South Africa',
continent: 'Africa',
},
{ code: 'kr', value: 'south-korea', label: 'South Korea', continent: 'Asia' },
{
code: 'gb',
value: 'united-kingdom',
label: 'United Kingdom',
continent: 'Europe',
},
{
code: 'us',
value: 'united-states',
label: 'United States',
continent: 'North America',
},
]
type Country = (typeof countries)[number]
export function ComboboxCustom() {
return () => (
<Combobox
items={countries.filter((country) => country.code !== '')}
itemToStringValue={(country: Country) => country.label}
>
<ComboboxInput placeholder="Search countries..." />
<ComboboxContent>
<ComboboxEmpty>No countries found.</ComboboxEmpty>
<ComboboxList>
{(country: Country) => (
<ComboboxItem key={country.code} value={country}>
<Item size="sm" mix={css({ padding: 0 })}>
<ItemContent>
<ItemTitle mix={css({ whiteSpace: 'nowrap' })}>{country.label}</ItemTitle>
<ItemDescription>
{country.continent} ({country.code})
</ItemDescription>
</ItemContent>
</Item>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Invalid
Use the aria-invalid prop to make the combobox invalid.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxInvalid() {
return () => (
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" aria-invalid="true" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Disabled
Use the disabled prop to disable the combobox.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxDisabled() {
return () => (
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" disabled />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Auto Highlight
Use the autoHighlight prop to automatically highlight the first item on filter.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from '#app/ui/combobox.tsx'
const frameworks = [
'Next.js',
'SvelteKit',
'Nuxt.js',
'Remix',
'Astro',
] as const
export function ComboboxAutoHighlight() {
return () => (
<Combobox items={frameworks} autoHighlight>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Popup
You can trigger the combobox from a button or any other component by using the render prop. Move the ComboboxInput inside the ComboboxContent.
import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
Combobox,
ComboboxContent,import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
} from '#app/ui/combobox.tsx'
const countries = [
{ code: '', value: '', continent: '', label: 'Select country' },
{
code: 'ar',
value: 'argentina',
label: 'Argentina',
continent: 'South America',
},
{ code: 'au', value: 'australia', label: 'Australia', continent: 'Oceania' },
{ code: 'br', value: 'brazil', label: 'Brazil', continent: 'South America' },
{ code: 'ca', value: 'canada', label: 'Canada', continent: 'North America' },
{ code: 'cn', value: 'china', label: 'China', continent: 'Asia' },
{
code: 'co',
value: 'colombia',
label: 'Colombia',
continent: 'South America',
},
{ code: 'eg', value: 'egypt', label: 'Egypt', continent: 'Africa' },
{ code: 'fr', value: 'france', label: 'France', continent: 'Europe' },
{ code: 'de', value: 'germany', label: 'Germany', continent: 'Europe' },
{ code: 'it', value: 'italy', label: 'Italy', continent: 'Europe' },
{ code: 'jp', value: 'japan', label: 'Japan', continent: 'Asia' },
{ code: 'ke', value: 'kenya', label: 'Kenya', continent: 'Africa' },
{ code: 'mx', value: 'mexico', label: 'Mexico', continent: 'North America' },
{
code: 'nz',
value: 'new-zealand',
label: 'New Zealand',
continent: 'Oceania',
},
{ code: 'ng', value: 'nigeria', label: 'Nigeria', continent: 'Africa' },
{
code: 'za',
value: 'south-africa',
label: 'South Africa',
continent: 'Africa',
},
{ code: 'kr', value: 'south-korea', label: 'South Korea', continent: 'Asia' },
{
code: 'gb',
value: 'united-kingdom',
label: 'United Kingdom',
continent: 'Europe',
},
{
code: 'us',
value: 'united-states',
label: 'United States',
continent: 'North America',
},
]
type Country = (typeof countries)[number]
export function ComboboxPopup() {
return () => (
<Combobox items={countries} defaultValue={countries[0]}>
<ComboboxTrigger
mix={[
buttonVariants({ variant: 'outline' }),
css({ width: '16rem', justifyContent: 'space-between', fontWeight: 400 }),
]}
>
<ComboboxValue />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxInput showTrigger={false} placeholder="Search" />
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item: Country) => (
<ComboboxItem key={item.code} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Input Group
You can add an addon to the combobox by using the InputGroupAddon component inside the ComboboxInput.
import { css } from 'remix/component'
import {
Combobox,
ComboboxCollection,
ComboboxContent,import { css } from 'remix/component'
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
} from '#app/ui/combobox.tsx'
import { GlobeIcon } from '#app/ui/icons.tsx'
import { InputGroupAddon } from '#app/ui/input-group.tsx'
const timezones = [
{
value: 'Americas',
items: [
'(GMT-5) New York',
'(GMT-8) Los Angeles',
'(GMT-6) Chicago',
'(GMT-5) Toronto',
'(GMT-8) Vancouver',
'(GMT-3) São Paulo',
],
},
{
value: 'Europe',
items: [
'(GMT+0) London',
'(GMT+1) Paris',
'(GMT+1) Berlin',
'(GMT+1) Rome',
'(GMT+1) Madrid',
'(GMT+1) Amsterdam',
],
},
{
value: 'Asia/Pacific',
items: [
'(GMT+9) Tokyo',
'(GMT+8) Shanghai',
'(GMT+8) Singapore',
'(GMT+4) Dubai',
'(GMT+11) Sydney',
'(GMT+9) Seoul',
],
},
] as const
type TimezoneGroup = (typeof timezones)[number]
export function ComboboxInputGroup() {
return () => (
<Combobox items={timezones}>
<ComboboxInput placeholder="Select a timezone">
<InputGroupAddon>
<GlobeIcon />
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent alignOffset={-28} mix={css({ width: '15rem' })}>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group: TimezoneGroup) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}API Reference
Combobox
| Prop | Type | Default |
|---|---|---|
items | readonly unknown[] | - |
multiple | boolean | - |
value | unknown | - |
defaultValue | unknown | - |
onValueChange | ItemCallback<void, []> | - |
open | boolean | - |
defaultOpen | boolean | - |
onOpenChange | ((open: boolean) => void) | - |
inputValue | string | - |
defaultInputValue | string | - |
onInputValueChange | ((value: string) => void) | - |
autoHighlight | boolean | - |
disabled | boolean | - |
itemToStringLabel | ItemCallback<string, []> | - |
itemToStringValue | ItemCallback<string, []> | - |
filter | ItemCallback<boolean, [query: string]> | - |
name | string | - |
ComboboxInput
| Prop | Type | Default |
|---|---|---|
type | "text" | - |
mix | MixValue | - |
showTrigger | boolean | true |
showClear | boolean | false |
ComboboxTrigger
Toggles the popup. Without ComboboxInput it is the anchor too, e.g. a button-styled trigger: <ComboboxTrigger mix={buttonVariants({ variant: 'outline' })}><ComboboxValue /></ComboboxTrigger>.
| Prop | Type | Default |
|---|---|---|
asInputGroupButton | boolean | - |
mix | MixValue | - |
ComboboxClear
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxValue
| Prop | Type | Default |
|---|---|---|
placeholder | RemixNode | - |
ComboboxContent
| Prop | Type | Default |
|---|---|---|
side | Side | "bottom" |
align | Align | "start" |
sideOffset | number | 6 |
alignOffset | number | 0 |
mix | MixValue | - |
ComboboxList
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxItem
| Prop | Type | Default |
|---|---|---|
value | unknown | - |
disabled | boolean | - |
mix | MixValue | - |
ComboboxGroup
| Prop | Type | Default |
|---|---|---|
items | readonly unknown[] | [] |
mix | MixValue | - |
ComboboxCollection
ComboboxLabel
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxEmpty
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxSeparator
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxChips
The chips container; it is also the popup's anchor (shadcn's useComboboxAnchor()).
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
ComboboxChip
| Prop | Type | Default |
|---|---|---|
value | unknown | - |
showRemove | boolean | true |
mix | MixValue | - |
ComboboxChipsInput
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
On This Page
InstallationUsageExamplesMultipleClear ButtonGroupsCustom ItemsInvalidDisabledAuto HighlightPopupInput GroupAPI ReferenceComboboxComboboxInputComboboxTriggerComboboxClearComboboxValueComboboxContentComboboxListComboboxItemComboboxGroupComboboxCollectionComboboxLabelComboboxEmptyComboboxSeparatorComboboxChipsComboboxChipComboboxChipsInput