Installation
npx @park-ui/cli add comboboxAdd Component
Copy the code snippet below into you components folder.
'use client'
import { Combobox, useComboboxItemContext } from '@ark-ui/react/combobox'
import { ark } from '@ark-ui/react/factory'
import { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'
import { forwardRef } from 'react'
import { createStyleContext, type HTMLStyledProps } from 'styled-system/jsx'
import { type ComboboxVariantProps, combobox } from 'styled-system/recipes'
const { withProvider, withContext } = createStyleContext(combobox)
export type RootProps = HTMLStyledProps<'div'> & ComboboxVariantProps
export const Root = withProvider(Combobox.Root, 'root', {
defaultProps: { positioning: { sameWidth: false } },
}) as Combobox.RootComponent<RootProps>
export const RootProvider = withProvider(
Combobox.RootProvider,
'root',
) as Combobox.RootProviderComponent<RootProps>
export const ClearTrigger = withContext(Combobox.ClearTrigger, 'clearTrigger', {
defaultProps: { children: <XIcon /> },
})
export const Content = withContext(Combobox.Content, 'content')
export const Control = withContext(Combobox.Control, 'control')
export const Empty = withContext(Combobox.Empty, 'empty')
export const IndicatorGroup = withContext(ark.div, 'indicatorGroup')
export const Input = withContext(Combobox.Input, 'input')
export const Item = withContext(Combobox.Item, 'item')
export const ItemGroup = withContext(Combobox.ItemGroup, 'itemGroup')
export const ItemGroupLabel = withContext(Combobox.ItemGroupLabel, 'itemGroupLabel')
export const ItemText = withContext(Combobox.ItemText, 'itemText')
export const Label = withContext(Combobox.Label, 'label')
export const List = withContext(Combobox.List, 'list')
export const Positioner = withContext(Combobox.Positioner, 'positioner')
export const Trigger = withContext(Combobox.Trigger, 'trigger', {
defaultProps: { children: <ChevronsUpDownIcon /> },
})
export { ComboboxContext as Context } from '@ark-ui/react/combobox'
const StyledItemIndicator = withContext(Combobox.ItemIndicator, 'itemIndicator')
export const ItemIndicator = forwardRef<HTMLDivElement, HTMLStyledProps<'div'>>(
function ItemIndicator(props, ref) {
const item = useComboboxItemContext()
return item.selected ? (
<StyledItemIndicator ref={ref} {...props}>
<CheckIcon />
</StyledItemIndicator>
) : (
<svg aria-hidden="true" focusable="false" />
)
},
)
Integrate Recipe
Integrate this recipe in to your Panda config.
import { comboboxAnatomy } from '@ark-ui/react/combobox'
import { defineSlotRecipe } from '@pandacss/dev'
import { input } from './input'
export const combobox = defineSlotRecipe({
className: 'combobox',
slots: comboboxAnatomy.extendWith('indicatorGroup').keys(),
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'full',
},
label: {
textStyle: 'label',
},
input: {
...input.base,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
control: {
position: 'relative',
},
content: {
background: 'gray.surface.bg',
borderRadius: 'l2',
boxShadow: 'md',
display: 'flex',
flexDirection: 'column',
maxH: 'min(var(--available-height), {sizes.96})',
minWidth: 'max(var(--reference-width), {sizes.40})',
outline: '0',
overflowY: 'auto',
zIndex: 'dropdown',
_open: {
animationStyle: 'slide-fade-in',
animationDuration: 'slow',
},
_closed: {
animationStyle: 'slide-fade-out',
animationDuration: 'fastest',
},
'&[data-empty]:not(:has([data-scope=combobox][data-part=empty]))': {
opacity: 0,
},
},
item: {
alignItems: 'center',
borderRadius: 'l1',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
_hover: {
background: 'gray.surface.bg.hover',
},
_highlighted: {
background: 'gray.surface.bg.hover',
},
_selected: {},
_disabled: {
layerStyle: 'disabled',
},
},
itemGroup: {
display: 'flex',
flexDirection: 'column',
},
itemGroupLabel: {
alignItems: 'flex-start',
color: 'fg.subtle',
display: 'flex',
flexDirection: 'column',
fontWeight: 'medium',
gap: '1px',
justifyContent: 'center',
_after: {
content: '""',
width: '100%',
height: '1px',
bg: 'border',
},
},
itemIndicator: {
color: 'colorPalette.plain.fg',
},
indicatorGroup: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '1',
pos: 'absolute',
insetEnd: '0',
top: '0',
bottom: '0',
},
trigger: {
color: 'fg.subtle',
},
clearTrigger: {
color: 'fg.muted',
},
empty: {
display: 'flex',
alignItems: 'center',
color: 'fg.subtle',
},
},
defaultVariants: {
size: 'md',
variant: 'outline',
},
variants: {
variant: {
outline: {
input: input.variants.variant.outline,
},
surface: {
input: input.variants.variant.surface,
},
subtle: {
input: input.variants.variant.subtle,
},
},
size: {
xs: {
input: {
...input.variants.size.xs,
pe: '12',
},
content: { p: '1', gap: '0.5', textStyle: 'sm' },
item: { px: '1', minH: '8', gap: '2', _icon: { boxSize: '3.5' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '1', height: '8' },
indicatorGroup: { px: '2', _icon: { boxSize: '3.5' } },
empty: { px: '1', minH: '8' },
},
sm: {
input: {
...input.variants.size.sm,
pe: '14',
},
content: { p: '1', gap: '0.5', textStyle: 'sm' },
item: { px: '1.5', minH: '9', gap: '2', _icon: { boxSize: '4' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '1.5', height: '9' },
indicatorGroup: { px: '2.5', _icon: { boxSize: '4' } },
empty: { px: '1.5', minH: '9' },
},
md: {
input: {
...input.variants.size.md,
pe: '14',
},
content: { p: '1', gap: '0.5', textStyle: 'md' },
indicatorGroup: { px: '3', _icon: { boxSize: '4' } },
item: { px: '2', minH: '10', gap: '2', _icon: { boxSize: '4' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '2', height: '10' },
empty: { px: '2', minH: '10' },
},
lg: {
input: {
...input.variants.size.lg,
pe: '16',
},
content: { p: '1', gap: '0.5', textStyle: 'md' },
item: { px: '2.5', minH: '11', gap: '2', _icon: { boxSize: '4.5' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '2.5', height: '11' },
indicatorGroup: { px: '3.5', _icon: { boxSize: '4.5' } },
empty: { px: '2.5', minH: '11' },
},
xl: {
input: {
...input.variants.size.xl,
pe: '16',
},
content: { p: '1', gap: '1', textStyle: 'lg' },
item: { px: '3', minH: '12', gap: '3', _icon: { boxSize: '5' } },
itemGroup: { gap: '1' },
itemGroupLabel: { px: '3', height: '12' },
indicatorGroup: { px: '4', _icon: { boxSize: '5' } },
empty: { px: '3', minH: '12' },
},
},
},
})
Usage
import { Combobox } from '@/components/ui'
<Combobox.Root>
<Combobox.Label />
<Combobox.Control>
<Combobox.Input />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty />
<Combobox.Item />
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel />
<Combobox.Item />
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
To setup combobox, you might need to import the following hooks:
useListCollection: Used to manage the list of items in the combobox, providing helpful methods for filtering and mutating the list.useFilter: Used to provide the filtering logic for the combobox based onIntl.CollatorAPIs.
Examples
Sizes
Use the size prop to adjust the size of the combobox.
Variants
Use the variant prop to adjust the visual style of the combobox.
Props
Root
| Prop | Default | Type |
|---|---|---|
collection* | ListCollection<T>The collection of items | |
variant | 'outline' | 'outline' | 'surface' | 'subtle' |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
alwaysSubmitOnEnter | false | booleanWhether to always submit on Enter key press, even if popup is open. Useful for single-field autocomplete forms where Enter should submit the form. |
composite | true | booleanWhether the combobox is a composed with other composite widgets like tabs |
defaultInputValue | \\ | stringThe initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
defaultValue | [] | string[]The initial value of the combobox's selected items when rendered. Use when you don't need to control the value of the combobox's selected items. |
inputBehavior | \none\ | 'none' | 'autohighlight' | 'autocomplete'Defines the auto-completion behavior of the combobox. - `autohighlight`: The first focused item is highlighted as the user types - `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated |
lazyMount | false | booleanWhether to enable lazy mounting |
loopFocus | true | booleanWhether to loop the keyboard navigation through the items |
openOnChange | true | boolean | ((details: InputValueChangeDetails) => boolean)Whether to show the combobox when the input value changes |
openOnClick | false | booleanWhether to open the combobox popup on initial click on the input |
openOnKeyPress | true | booleanWhether to open the combobox on arrow key press |
positioning | { placement: \bottom-start\ } | PositioningOptionsThe positioning options to dynamically position the menu |
selectionBehavior | \replace\ | 'clear' | 'replace' | 'preserve'The behavior of the combobox input when an item is selected - `replace`: The selected item string is set as the input value - `clear`: The input value is cleared - `preserve`: The input value is preserved |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
unmountOnExit | false | booleanWhether to unmount on exit. |
allowCustomValue | booleanWhether to allow typing custom values in the input | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
autoFocus | booleanWhether to autofocus the input on mount | |
closeOnSelect | booleanWhether to close the combobox when an item is selected. | |
defaultHighlightedValue | stringThe initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. | |
defaultOpen | booleanThe initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. | |
disabled | booleanWhether the combobox is disabled | |
disableLayer | booleanWhether to disable registering this a dismissable layer | |
form | stringThe associate form of the combobox. | |
highlightedValue | stringThe controlled highlighted value of the combobox | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
label: string
control: string
input: string
content: string
trigger: string
clearTrigger: string
item: (id: string, index?: number | undefined) => string
positioner: string
itemGroup: (id: string | number) => string
itemGroupLabel: (id: string | number) => string
}>The ids of the elements in the combobox. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
inputValue | stringThe controlled value of the combobox's input | |
invalid | booleanWhether the combobox is invalid | |
multiple | booleanWhether to allow multiple selection. **Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`. It is recommended to render the selected items in a separate container. | |
name | stringThe `name` attribute of the combobox's input. Useful for form submission | |
navigate | (details: NavigateDetails) => voidFunction to navigate to the selected item | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails<T>) => voidFunction called when an item is highlighted using the pointer or keyboard navigation. | |
onInputValueChange | (details: InputValueChangeDetails) => voidFunction called when the input's value changes | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => voidFunction called when the popup is opened | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onSelect | (details: SelectionDetails) => voidFunction called when an item is selected | |
onValueChange | (details: ValueChangeDetails<T>) => voidFunction called when a new item is selected | |
open | booleanThe controlled open state of the combobox | |
placeholder | stringThe placeholder text of the combobox's input | |
present | booleanWhether the node is present (controlled by the user) | |
readOnly | booleanWhether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it | |
required | booleanWhether the combobox is required | |
scrollToIndexFn | (details: ScrollToIndexDetails) => voidFunction to scroll to a specific index | |
translations | IntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their states | |
value | string[]The controlled value of the combobox's selected items |
Item
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
item | anyThe item to render | |
persistFocus | booleanWhether hovering outside should clear the highlighted state |
Trigger
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
focusable | booleanWhether the trigger is focusable |