Combobox
A single input field that combines the functionality of a select and input.
'use client'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import { useState } from 'react'
import { Combobox, createListCollection } from '~/components/ui/combobox'
import { IconButton } from '~/components/ui/icon-button'
import { Input } from '~/components/ui/input'
const initialCollection = createListCollection({
items: [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte', disabled: true },
],
})
export const Demo = (props: Combobox.RootProps) => {
const [collection, setCollection] = useState(initialCollection)
const handleInputChange = ({ inputValue }: Combobox.InputValueChangeDetails) => {
const filtered = initialCollection.items.filter((item) =>
item.label.toLowerCase().includes(inputValue.toLowerCase()),
)
setCollection(
filtered.length > 0 ? createListCollection({ items: filtered }) : initialCollection,
)
}
const handleOpenChange = () => {
setCollection(initialCollection)
}
return (
<Combobox.Root
{...props}
width="2xs"
collection={collection}
onInputValueChange={handleInputChange}
onOpenChange={handleOpenChange}
>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Select a Framework" asChild>
<Input />
</Combobox.Input>
<Combobox.Trigger asChild>
<IconButton variant="link" aria-label="open" size="xs">
<ChevronsUpDownIcon />
</IconButton>
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
{collection.items.map((item) => (
<Combobox.Item key={item.value} item={item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
)
}
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-solid'
import { For, createSignal } from 'solid-js'
import { Combobox, createListCollection } from '~/components/ui/combobox'
import { IconButton } from '~/components/ui/icon-button'
import { Input } from '~/components/ui/input'
const data = [
{ label: 'React', value: 'react' },
{ label: 'Solid', value: 'solid' },
{ label: 'Svelte', value: 'svelte', disabled: true },
{ label: 'Vue', value: 'vue' },
]
export const Demo = (props: Combobox.RootProps) => {
const [items, setItems] = createSignal(data)
const collection = createListCollection({
items: data,
})
const handleChange = (e: Combobox.InputValueChangeDetails) => {
const filtered = data.filter((item) =>
item.label.toLowerCase().includes(e.inputValue.toLowerCase()),
)
setItems(filtered.length > 0 ? filtered : data)
}
return (
<Combobox.Root width="2xs" onInputValueChange={handleChange} {...props} collection={collection}>
<Combobox.Label>Framework</Combobox.Label>
<Combobox.Control>
<Combobox.Input
placeholder="Select a Framework"
asChild={(inputProps) => <Input {...inputProps()} />}
/>
<Combobox.Trigger
asChild={(triggerProps) => (
<IconButton variant="link" aria-label="open" size="xs" {...triggerProps()}>
<ChevronsUpDownIcon />
</IconButton>
)}
/>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.ItemGroup>
<Combobox.ItemGroupLabel>Frameworks</Combobox.ItemGroupLabel>
<For each={items()}>
{(item) => (
<Combobox.Item item={item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
<Combobox.ItemIndicator>
<CheckIcon />
</Combobox.ItemIndicator>
</Combobox.Item>
)}
</For>
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
)
}
Usage
import { Combobox, createListCollection } from '~/components/ui/combobox'
Installation
npx @park-ui/cli components add combobox
1
Add Styled Primitive
Copy the code snippet below into ~/components/ui/styled/combobox.tsx
'use client'
import type { Assign } from '@ark-ui/react'
import { Combobox } from '@ark-ui/react/combobox'
import { type ComboboxVariantProps, combobox } from 'styled-system/recipes'
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withProvider, withContext } = createStyleContext(combobox)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
HTMLDivElement,
Assign<
Assign<HTMLStyledProps<'div'>, Combobox.RootProviderBaseProps<Combobox.CollectionItem>>,
ComboboxVariantProps
>
>(Combobox.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
HTMLDivElement,
Assign<
Assign<HTMLStyledProps<'div'>, Combobox.RootBaseProps<Combobox.CollectionItem>>,
ComboboxVariantProps
>
>(Combobox.Root, 'root')
export const ClearTrigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, Combobox.ClearTriggerBaseProps>
>(Combobox.ClearTrigger, 'clearTrigger')
export const Content = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ContentBaseProps>
>(Combobox.Content, 'content')
export const Control = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ControlBaseProps>
>(Combobox.Control, 'control')
export const Input = withContext<
HTMLInputElement,
Assign<HTMLStyledProps<'input'>, Combobox.InputBaseProps>
>(Combobox.Input, 'input')
export const ItemGroupLabel = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ItemGroupLabelBaseProps>
>(Combobox.ItemGroupLabel, 'itemGroupLabel')
export const ItemGroup = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ItemGroupBaseProps>
>(Combobox.ItemGroup, 'itemGroup')
export const ItemIndicator = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ItemIndicatorBaseProps>
>(Combobox.ItemIndicator, 'itemIndicator')
export const Item = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ItemBaseProps>
>(Combobox.Item, 'item')
export const ItemText = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'span'>, Combobox.ItemTextBaseProps>
>(Combobox.ItemText, 'itemText')
export const Label = withContext<
HTMLLabelElement,
Assign<HTMLStyledProps<'label'>, Combobox.LabelBaseProps>
>(Combobox.Label, 'label')
export const List = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.ListBaseProps>
>(Combobox.List, 'list')
export const Positioner = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Combobox.PositionerBaseProps>
>(Combobox.Positioner, 'positioner')
export const Trigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, Combobox.TriggerBaseProps>
>(Combobox.Trigger, 'trigger')
export { ComboboxContext as Context } from '@ark-ui/react/combobox'
export type {
ComboboxHighlightChangeDetails as HighlightChangeDetails,
ComboboxInputValueChangeDetails as InputValueChangeDetails,
ComboboxOpenChangeDetails as OpenChangeDetails,
ComboboxValueChangeDetails as ValueChangeDetails,
} from '@ark-ui/react/combobox'
import { type Assign, Combobox } from '@ark-ui/solid'
import type { ComponentProps } from 'solid-js'
import { type ComboboxVariantProps, combobox } from 'styled-system/recipes'
import type { HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withRootProvider, withContext } = createStyleContext(combobox)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withRootProvider<
Assign<
Assign<HTMLStyledProps<'div'>, Combobox.RootProviderBaseProps<Combobox.CollectionItem>>,
ComboboxVariantProps
>
>(Combobox.RootProvider)
export type RootProps = ComponentProps<typeof Root>
export const Root = withRootProvider<
Assign<
Assign<HTMLStyledProps<'div'>, Combobox.RootBaseProps<Combobox.CollectionItem>>,
ComboboxVariantProps
>
>(Combobox.Root)
export const ClearTrigger = withContext<
Assign<HTMLStyledProps<'button'>, Combobox.ClearTriggerBaseProps>
>(Combobox.ClearTrigger, 'clearTrigger')
export const Content = withContext<Assign<HTMLStyledProps<'div'>, Combobox.ContentBaseProps>>(
Combobox.Content,
'content',
)
export const Control = withContext<Assign<HTMLStyledProps<'div'>, Combobox.ControlBaseProps>>(
Combobox.Control,
'control',
)
export const Input = withContext<Assign<HTMLStyledProps<'input'>, Combobox.InputBaseProps>>(
Combobox.Input,
'input',
)
export const ItemGroupLabel = withContext<
Assign<HTMLStyledProps<'div'>, Combobox.ItemGroupLabelBaseProps>
>(Combobox.ItemGroupLabel, 'itemGroupLabel')
export const ItemGroup = withContext<Assign<HTMLStyledProps<'div'>, Combobox.ItemGroupBaseProps>>(
Combobox.ItemGroup,
'itemGroup',
)
export const ItemIndicator = withContext<
Assign<HTMLStyledProps<'div'>, Combobox.ItemIndicatorBaseProps>
>(Combobox.ItemIndicator, 'itemIndicator')
export const Item = withContext<Assign<HTMLStyledProps<'div'>, Combobox.ItemBaseProps>>(
Combobox.Item,
'item',
)
export const ItemText = withContext<Assign<HTMLStyledProps<'span'>, Combobox.ItemTextBaseProps>>(
Combobox.ItemText,
'itemText',
)
export const Label = withContext<Assign<HTMLStyledProps<'label'>, Combobox.LabelBaseProps>>(
Combobox.Label,
'label',
)
export const List = withContext<Assign<HTMLStyledProps<'div'>, Combobox.ListBaseProps>>(
Combobox.List,
'list',
)
export const Positioner = withContext<Assign<HTMLStyledProps<'div'>, Combobox.PositionerBaseProps>>(
Combobox.Positioner,
'positioner',
)
export const Trigger = withContext<Assign<HTMLStyledProps<'button'>, Combobox.TriggerBaseProps>>(
Combobox.Trigger,
'trigger',
)
export { ComboboxContext as Context } from '@ark-ui/solid'
export type {
ComboboxHighlightChangeDetails as HighlightChangeDetails,
ComboboxInputValueChangeDetails as InputValueChangeDetails,
ComboboxOpenChangeDetails as OpenChangeDetails,
ComboboxValueChangeDetails as ValueChangeDetails,
} from '@ark-ui/solid'
No snippet found
2
Add Re-Export
To improve the developer experience, re-export the styled primitives in~/components/ui/combobox.tsx
.
export { createListCollection } from '@ark-ui/react/combobox'
export * as Combobox from './styled/combobox'
export { createListCollection } from '@ark-ui/solid/combobox'
export * as Combobox from './styled/combobox'
3
Integrate Recipe
If you're not using @park-ui/preset
, add the following recipe to yourpanda.config.ts
:
import { comboboxAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'
export const combobox = defineSlotRecipe({
className: 'combobox',
slots: comboboxAnatomy.keys(),
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'full',
},
control: {
position: 'relative',
},
label: {
color: 'fg.default',
fontWeight: 'medium',
},
trigger: {
bottom: '0',
color: 'fg.muted',
position: 'absolute',
right: '0',
top: '0',
},
content: {
background: 'bg.default',
borderRadius: 'l2',
boxShadow: 'lg',
display: 'flex',
flexDirection: 'column',
zIndex: 'dropdown',
_hidden: {
display: 'none',
},
_open: {
animation: 'fadeIn 0.25s ease-out',
},
_closed: {
animation: 'fadeOut 0.2s ease-out',
},
_focusVisible: {
outlineOffset: '2px',
outline: '2px solid',
outlineColor: 'border.outline',
},
},
item: {
alignItems: 'center',
borderRadius: 'l1',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
transitionDuration: 'fast',
transitionProperty: 'background, color',
transitionTimingFunction: 'default',
_hover: {
background: 'bg.muted',
},
_highlighted: {
background: 'bg.muted',
},
_disabled: {
color: 'fg.disabled',
cursor: 'not-allowed',
_hover: {
background: 'transparent',
},
},
},
itemGroupLabel: {
fontWeight: 'semibold',
textStyle: 'sm',
},
itemIndicator: {
color: 'colorPalette.default',
},
},
defaultVariants: {
size: 'md',
},
variants: {
size: {
sm: {
content: { p: '0.5', gap: '1' },
item: { textStyle: 'sm', px: '2', height: '9' },
itemIndicator: {
'& :where(svg)': {
width: '4',
height: '4',
},
},
itemGroupLabel: {
px: '2',
py: '1.5',
},
label: { textStyle: 'sm' },
trigger: { right: '2.5' },
},
md: {
content: { p: '1', gap: '1' },
item: { textStyle: 'md', px: '2', height: '10' },
itemIndicator: {
'& :where(svg)': {
width: '4',
height: '4',
},
},
itemGroupLabel: {
px: '2',
py: '1.5',
},
label: { textStyle: 'sm' },
trigger: { right: '3' },
},
lg: {
content: { p: '1.5', gap: '1' },
item: { textStyle: 'md', px: '2', height: '11' },
itemIndicator: {
'& :where(svg)': {
width: '5',
height: '5',
},
},
itemGroupLabel: {
px: '2',
py: '1.5',
},
label: { textStyle: 'sm' },
trigger: { right: '3.5' },
},
},
},
})