A filterable select component that allows users to search and select options.
import {
Combobox,
ComboItemWithIndicator,
ComboItemText,
} from '@cerberus/react'The Combobox is an abracted API that combines multiple primitives into a single root component. You combine this with the useStatefulCollection hook to create a filterable select component.
To add an icon to the start position of the input, use the startIcon prop to pass in your icon of choice.
The Combobox component supports sizes: sm to lg. The default is md.
Wrap the Combobox component with the Field component to add additional functionality such as error and helper text when using it in a form.
Access the combobox's state with ComboboxContext or the useComboboxContext hook—useful for displaying the selected value or building custom UI.
Note
Since combobox is an abstracted API, you'll need to build your own to obtain the context as we do in the example below.
To group items, use the ComboItemGroup component (not ComboboxItemGroup).
Combine the collection with a query to load data asynchronously with signal-based speed.
Combine the ComboItemText component with the Highlight component to highlight matching text in the dropdown.
Allow users to create new options when their search doesn't match any existing items. This is useful for tags, categories, or other custom values.
Customize the navigate prop on Combobox to integrate with your router. Using Tanstack Router:
import { Combobox } from '@cerberus/react'
import { useNavigate } from '@tanstack/react-router'
function Demo() {
const navigate = useNavigate()
return (
<Combobox
navigate={(e) => {
navigate({ to: e.node.href })
}}
>
{/* ... */}
</Combobox>
)
}By default, the combobox collection expects an array of objects with label and value properties. In some cases, you may need to deal with custom objects.
Use the itemToString and itemToValue props to map the custom object to the required interface.
const items = [
{ country: 'United States', code: 'US', flag: '🇺🇸' },
{ country: 'Canada', code: 'CA', flag: '🇨🇦' },
{ country: 'Australia', code: 'AU', flag: '🇦🇺' },
// ...
]
const { collection } = useListCollection({
initialItems: items,
itemToString: (item) => item.country,
itemToValue: (item) => item.code,
})The ComboboxRootProps type enables you to create typed wrapper components that maintain full type safety for collection items.
const Combobox = (props: ComboboxRootProps) => {
return <CerbyCombobox {...props}>{/* ... */}</CerbyCombobox>
}The recommended way of managing large lists is to use the limit property on the useListCollection hook. This will limit the number of rendered items in the DOM to improve performance.
const { collection } = useListCollection({
initialItems: items,
limit: 10,
})The following css variables are exposed to the ComboboxPositioner which you can use to style the ComboboxContent.
/* width of the combobox control */
--reference-width: <pixel-value>;
/* width of the available viewport */
--available-width: <pixel-value>;
/* height of the available viewport */
--available-height: <pixel-value>;For example, if you want to make sure the maximum height doesn't exceed the available height, you can use the following:
[data-scope='combobox'][data-part='content'] {
max-height: calc(var(--available-height) - 100px);
}You can utilize the primitive components or the css prop to customize the Combobox.
| Component | Description |
|---|---|
| ComboboxRoot | The context provider for the combobox family |
| ComboboxLabel | The label that appears above the combobox input |
| ComboboxControl | The wrapper to the combobox trigger that opens the dropdown |
| ComboboxInput | The input field of the combobox |
| ComboboxTrigger | he trigger that opens the dropdown |
| ComboboxClearTrigger | The trigger that clears the selected value |
| ComboboxPositioner | The wrapper that positions the dropdown |
| ComboboxContent | The content of the dropdown (i.e. the container itself) |
| ComboboxItemGroup | The group of options in the dropdown |
| ComboboxItemGroupLabel | The label for the group of options |
| ComboboxItem | The option in the dropdown |
| ComboboxItemText | The text label of the option |
| ComboboxItemIndicator | The indicator shown when the option is selected |
The Combobox component is an abstraction of our primitives and accepts the following props:
| Name | Default | Description |
|---|---|---|
size | md | The size of the combobox. |
startIcon | The icon to display at the start of the input. |
The Combobox component also accepts all the props of the ComboboxRoot primitive props
The ItemWithIndicator component is an abstraction of our primitives and accepts all the props of the ComboboxItem primitive props
The ComboItemGroup component is an abstraction of our primitives and accepts the following props:
| Name | Default | Description |
|---|---|---|
label | The label of the group. |
The ComboItemGroup component is an abstraction of our primitives and accepts all the props of the ComboboxItemGroup primitive props
| Prop | Type | Required | Description |
|---|---|---|---|
collection | ListCollection<T> | Yes | The collection of items |
allowCustomValue | boolean | No | Whether to allow typing custom values in the input |
alwaysSubmitOnEnter | boolean | No | Whether to always submit on Enter key press, even if popup is open. |
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
autoFocus | boolean | No | Whether to autofocus the input on mount |
closeOnSelect | boolean | No | Whether to close the combobox when an item is selected. |
autoFocus | boolean | No | Whether to autofocus the input on mount |
closeOnSelect | boolean | No | Whether to close the combobox when an item is selected. |
composite | boolean | No | Whether the combobox is a composed with other composite widgets like tabs |
defaultHighlightedValue | string | No | The initial highlighted value of the combobox when rendered. Use when you don't need to control the highlighted value of the combobox. |
defaultInputValue | string | No | The initial value of the combobox's input when rendered. Use when you don't need to control the value of the combobox's input. |
defaultOpen | boolean | No | The initial open state of the combobox when rendered. Use when you don't need to control the open state of the combobox. |
defaultValue | string[] | No | 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. |
disabled | boolean | No | Whether the combobox is disabled |
disableLayer | boolean | No | Whether to disable registering this a dismissable layer |
form | string | No | The associate form of the combobox. |
highlightedValue | string | No | The controlled highlighted value of the combobox |
id | string | No | The unique identifier of the machine. |
ids | Partial<Parts> | No | The ids of the elements in the combobox. Useful for composition. |
immediate | boolean | No | Whether to synchronize the present change immediately or defer it to the next frame |
inputBehavior | 'none' | 'autohighlight' | 'autocomplete' | No | 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 |
inputValue | string | No | The controlled value of the combobox's input |
invalid | boolean | No | Whether the combobox is invalid |
lazyMount | boolean | No | Whether to enable lazy mounting |
loopFocus | boolean | No | Whether to loop the keyboard navigation through the items |
multiple | boolean | No | Whether 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 | string | No | The name attribute of the combobox's input. Useful for form submission |
navigate | (details: NavigateDetails) => void | No | Function to navigate to the selected item |
onExitComplete | VoidFunction | No | Function called when the animation ends in the closed state |
onFocusOutside | (event: FocusOutsideEvent) => void | No | Function called when the focus is moved outside the component |
onHighlightChange | (details: HighlightChangeDetails<T>) => void | No | Function called when an item is highlighted using the pointer or keyboard navigation. |
onInputValueChange | (details: InputValueChangeDetails) => void | No | Function called when the input's value changes |
onInteractOutside | (event: InteractOutsideEvent) => void | No | Function called when an interaction happens outside the component |
onOpenChange | (details: OpenChangeDetails) => void | No | Function called when the popup is opened |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | No | Function called when the pointer is pressed down outside the component |
onSelect | (details: SelectionDetails) => void | No | Function called when an item is selected |
onValueChange | (details: ValueChangeDetails<T>) => void | No | Function called when a new item is selected |
open | boolean | No | The controlled open state of the combobox |
openOnChange | boolean | No | Whether to show the combobox when the input value changes |
openOnClick | boolean | No | Whether to open the combobox popup on initial click on the input |
openOnKeyPress | boolean | No | Whether to open the combobox on arrow key press |
placeholder | string | No | The placeholder text of the combobox's input |
positioning | PositioningOptions | No | The positioning options to dynamically position the menu |
present | boolean | No | Whether the node is present (controlled by the user) |
readOnly | boolean | No | Whether the combobox is readonly. This puts the combobox in a "non-editable" mode but the user can still interact with it |
required | boolean | No | Whether the combobox is required |
scrollToIndexFn | (details: ScrollToIndexDetails) => void | No | Function to scroll to a specific index |
selectionBehavior | 'clear' | 'replace' | 'preserve' | No | 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 | boolean | No | Whether to allow the initial presence animation. |
translations | IntlTranslations | No | Specifies the localized strings that identifies the accessibility elements and their states |
unmountOnExit | boolean | No | Whether to unmount on exit. |
value | string[] | No | No | The controlled value of the combobox's selected items |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | root |
[data-invalid] | Present when invalid |
[data-readonly] | Present when read-only |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | clear-trigger |
[data-invalid] | Present when invalid |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | content |
[data-state] | "open" | "closed" |
[data-nested] | listbox |
[data-has-nested] | listbox |
[data-placement] | The placement of the content |
[data-empty] | Present when the content is empty |
| Variable | Description |
|---|---|
--layer-index | The index of the dismissable in the layer stack |
--nested-layer-count | The number of nested comboboxs |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | control |
[data-state] | "open" | "closed" |
[data-focus] | Present when focused |
[data-disabled] | Present when disabled |
[data-invalid] | Present when invalid |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | input |
[data-invalid] | Present when invalid |
[data-autofocus] | |
[data-state] | "open" | "closed" |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-group |
[data-empty] | Present when the content is empty |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-indicator |
[data-state] | "checked" | "unchecked" |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
item | any | No | The item to render |
persistFocus | boolean | No | Whether hovering outside should clear the highlighted state |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item |
[data-highlighted] | Present when highlighted |
[data-state] | "checked" | "unchecked" |
[data-disabled] | Present when disabled |
[data-value] | The value of the item |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | item-text |
[data-state] | "checked" | "unchecked" |
[data-disabled] | Present when disabled |
[data-highlighted] | Present when highlighted |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | label |
[data-readonly] | Present when read-only |
[data-disabled] | Present when disabled |
[data-invalid] | Present when invalid |
[data-required] | Present when required |
[data-focus] | Present when focused |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | list |
[data-empty] | Present when the content is empty |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Variable | Description |
|---|---|
--reference-width | The width of the reference element |
--reference-height | The height of the root |
--available-width | The available width in viewport |
--available-height | The available height in viewport |
--x | The x position for transform |
--y | The y position for transform |
--z-index | The z-index value |
--transform-origin | The transform origin for animations |
| Prop | Type | Required | Description |
|---|---|---|---|
value | UseComboboxReturn<T> | Yes | |
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
immediate | boolean | No | Whether to synchronize the present change immediately or defer it to the next frame |
lazyMount | boolean | No | Whether to enable lazy mounting |
onExitComplete | VoidFunction | No | Function called when the animation ends in the closed state |
present | boolean | No | Whether the node is present (controlled by the user) |
skipAnimationOnMount | boolean | No | Whether to allow the initial presence animation. |
unmountOnExit | boolean | No | Whether to unmount on exit. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
focusable | boolean | No | Whether the trigger is focusable |
| Attribute | Value |
|---|---|
[data-scope] | combobox |
[data-part] | trigger |
[data-state] | "open" | "closed" |
[data-invalid] | Present when invalid |
[data-focusable] | |
[data-readonly] | Present when read-only |
[data-disabled] | Present when disabled |
The useStatefulCollection function is a utility hook that creates a collection of options and filters the list based on the user input.
| Name | Description |
|---|---|
| collection | The collection of options. |
| filterChars | The filter value split into an Array of chars. |
| handleInputChange | The function to pass to onInputValueChange. |
The ComboboxParts API is an Object containing the full family of components.
Note
It is best to only use the ComboboxParts if you are building a custom solution. Importing Object based components will ship every property it includes into your bundle, regardless if you use it or not.
| Name | Description |
|---|---|
| Root | The ComboboxRoot component which is the Provider for the family. |
| Label | The ComboboxLabel component which displays the label. |
| Control | The ComboboxControl component which is the container for the visual field. |
| Input | The ComboboxInput component which is the visual field. |
| Trigger | The ComboboxTrigger component which is the trigger for the dropdown. |
| ClearTrigger | The ComboboxClearTrigger component which is the trigger to clear the value. |
| Positioner | The ComboboxPositioner component which is controls the positioning for the dropdown. |
| Content | The ComboboxContent component which is the dropdown itself. |
| ItemGroup | The ComboboxItemGroup component which is the group of options in the dropdown. |
| ItemGroupLabel | The ComboboxItemGroupLabel component which is the label for the group of options. |
| Item | The ComboboxItem component which is the option in the dropdown. |
| ItemText | The ComboboxItemText component which is the text label of the option. |
| ItemIndicator | The ComboboxItemIndicator component which displays based on the checked state. |
On this page
Loading...
Loading...
Loading...
Loading...
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import {
Combobox,
ComboItemText,
ComboItemWithIndicator,
For,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function BasicDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<Combobox
collection={collection}
label="Select Relative"
onInputValueChange={handleInputChange}
placeholder="Choose option"
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
</Box>
)
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import { Search } from '@carbon/icons-react'
import {
Combobox,
ComboItemText,
ComboItemWithIndicator,
For,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function StartIconDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<Combobox
collection={collection}
label="Select Relative"
onInputValueChange={handleInputChange}
openOnClick
placeholder="Choose option"
startIcon={<Search />}
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
</Box>
)
}
'use client'
import { Stack, VStack } from '@/styled-system/jsx'
import {
Combobox,
ComboboxProps,
ComboItemText,
ComboItemWithIndicator,
For,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
import { sizes } from './meta'
export function SizeDemo() {
return (
<Stack gap="md" w="3/4">
<For each={sizes}>{(size) => <SizeBox key={String(size)} size={size} />}</For>
</Stack>
)
}
interface SizeBoxProps<T> {
size: ComboboxProps<T>['size']
}
export function SizeBox<T>(props: SizeBoxProps<T>) {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Combobox
collection={collection}
label={`Size: ${props.size}`}
onInputValueChange={handleInputChange}
placeholder="Type to search"
size={props.size}
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
)
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import {
Combobox,
ComboItemText,
ComboItemWithIndicator,
Field,
For,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function FieldDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<Field
label="Select Relative"
helperText="This is saved to your profile"
errorText="You must select something."
required
>
<Combobox
collection={collection}
onInputValueChange={handleInputChange}
placeholder="Choose option"
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
</Field>
</Box>
)
}
Selected: None
'use client'
import { Stack, VStack } from '@/styled-system/jsx'
import { ChevronDown, Close } from '@carbon/icons-react'
import {
ComboboxParts,
ComboItemText,
ComboItemWithIndicator,
For,
Text,
UseComboboxContext,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function ContextDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Stack w="1/2">
<ComboboxParts.Root
collection={collection}
onInputValueChange={handleInputChange}
placeholder="Choose option"
>
<ComboboxParts.Context>
{(context: UseComboboxContext<(typeof collection.items)[number]>) => (
<Text mb="md">Selected: {context.valueAsString || 'None'}</Text>
)}
</ComboboxParts.Context>
<ComboboxParts.Label>Select Relative</ComboboxParts.Label>
<ComboboxParts.Control>
<ComboboxParts.Input />
<ComboboxParts.ClearTrigger>
<Close />
</ComboboxParts.ClearTrigger>
<ComboboxParts.Trigger>
<ChevronDown />
</ComboboxParts.Trigger>
</ComboboxParts.Control>
<ComboboxParts.Positioner>
<ComboboxParts.Content>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</ComboboxParts.Content>
</ComboboxParts.Positioner>
</ComboboxParts.Root>
</Stack>
)
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import { Search } from '@carbon/icons-react'
import {
Combobox,
ComboItemGroup,
ComboItemText,
ComboItemWithIndicator,
For,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function GroupedItemsDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<Combobox
collection={collection}
label="Select Relative"
onInputValueChange={handleInputChange}
placeholder="Choose option"
startIcon={<Search />}
>
<ComboItemGroup label="The fam">
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>{item.label}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</ComboItemGroup>
</Combobox>
</Box>
)
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import {
Combobox,
ComboboxContext,
ComboItemText,
ComboItemWithIndicator,
For,
Highlight,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function HighlightDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<Combobox
collection={collection}
label="Select Relative"
onInputValueChange={handleInputChange}
placeholder="Choose option"
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<ComboItemText>
<ComboboxContext>
{(context) => (
<Highlight
text={item.label}
query={context.inputValue}
ignoreCase
/>
)}
</ComboboxContext>
</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
</Box>
)
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import { ChevronDownOutline } from '@carbon/icons-react'
import {
ComboboxParts,
For,
Portal,
Text,
useStatefulCollection,
} from '@cerberus/react'
import { items } from './items'
export function CustomDemo() {
const { collection, handleInputChange } = useStatefulCollection(items)
return (
<Box w="1/2">
<ComboboxParts.Root
collection={collection}
onInputValueChange={handleInputChange}
transform="skewX(-10deg)"
>
<ComboboxParts.Label textTransform="uppercase">
Custom label
</ComboboxParts.Label>
<ComboboxParts.Control>
<ComboboxParts.Input />
<ComboboxParts.Trigger>
<ChevronDownOutline />
</ComboboxParts.Trigger>
</ComboboxParts.Control>
<Portal>
<ComboboxParts.Positioner>
<ComboboxParts.Content>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboboxParts.Item key={item.value} item={item}>
<ComboboxParts.ItemText fontSize="xl">
{item.label}
</ComboboxParts.ItemText>
<ComboboxParts.ItemIndicator>🔥</ComboboxParts.ItemIndicator>
</ComboboxParts.Item>
)}
</For>
</ComboboxParts.Content>
</ComboboxParts.Positioner>
</Portal>
</ComboboxParts.Root>
</Box>
)
}
'use client'
import { Box, Square, VStack } from '@/styled-system/jsx'
import { Search } from '@carbon/icons-react'
import {
ComboItemText,
ComboItemWithIndicator,
Combobox,
ComboboxInputValueChangeDetails,
For,
Spinner,
Text,
createListCollection,
} from '@cerberus/react'
import { createQuery, useQuery } from '@cerberus/signals'
import { Suspense, useState, useTransition } from 'react'
function useDeferredValue() {
// Use native React state and transitions for loading state to override Suspsense
const [inputValue, setInputValue] = useState<string>('')
const [pending, startTransition] = useTransition()
return {
inputValue,
setInputValue,
pending,
startTransition,
}
}
function AsyncCombobox() {
const { inputValue, setInputValue, pending, startTransition } = useDeferredValue()
const data = useQuery(queryList(inputValue))
const collection = createListCollection<Character>({
items: data,
itemToString: (item) => item.name,
itemToValue: (item) => item.url,
})
const handleInputChange = (details: ComboboxInputValueChangeDetails) => {
if (details.reason === 'input-change') {
startTransition(() => {
setInputValue(details.inputValue)
})
}
}
return (
<Combobox
collection={collection}
label="Select Star Wars Character"
onInputValueChange={handleInputChange}
placeholder="Type to search or choose an option"
startIcon={
pending ? (
<Square size="4">
<Spinner />
</Square>
) : (
<Search />
)
}
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.name} item={item}>
<ComboItemText>{item.name}</ComboItemText>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
)
}
export function LoadingDemo() {
return (
<Box w="1/2">
<Suspense fallback="...loading">
<AsyncCombobox />
</Suspense>
</Box>
)
}
// Factories
interface Character {
name: string
height: string
mass: string
created: string
edited: string
url: string
}
const queryList = createQuery(async (inputValue: string) => {
try {
const response = await db.searchCharacters(inputValue)
return response.results
} catch (error) {
console.error(error)
return []
}
}, 'queryList')
// API
const db = {
searchCharacters: async (inputValue: string) => {
const response = await fetch(
`https://swapi.py4e.com/api/people/?search=${inputValue ?? ''}`,
)
return await response.json()
},
}
'use client'
import { Box, VStack } from '@/styled-system/jsx'
import { Corn } from '@carbon/icons-react'
import {
Combobox,
ComboboxInputValueChangeDetails,
ComboboxOpenChangeDetails,
ComboboxValueChangeDetails,
ComboItemText,
ComboItemWithIndicator,
For,
Show,
Tag,
Text,
useFilter,
useListCollection,
} from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
import { flushSync } from 'react-dom'
interface Item {
label: string
value: string
__new__?: boolean
}
const NEW_OPTION_VALUE = '[[new]]'
const api = {
create: (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE }),
isNew: (value: string) => value === NEW_OPTION_VALUE,
replaceValue: (values: string[], value: string) => {
return values.map((v) => (v === NEW_OPTION_VALUE ? value : v))
},
getNewOptionData: (inputValue: string): Item => ({
label: inputValue,
value: inputValue,
__new__: true,
}),
}
export function CreatableDemo() {
const [selectedValue, setSelectedValue] = useSignal<string[]>([])
const [inputValue, setInputValue] = useSignal('')
const { contains } = useFilter({ sensitivity: 'base' })
const { collection, filter, upsert, update, remove } = useListCollection<Item>({
initialItems: [
{ label: 'Bug', value: 'bug' },
{ label: 'Feature', value: 'feature' },
{ label: 'Enhancement', value: 'enhancement' },
{ label: 'Documentation', value: 'docs' },
],
filter: contains,
})
const isValidNewOption = (inputValue: string) => {
const exactOptionMatch =
collection.filter((item) => item.toLowerCase() === inputValue.toLowerCase())
.size > 0
return !exactOptionMatch && inputValue.trim().length > 0
}
const handleInputChange = ({
inputValue,
reason,
}: ComboboxInputValueChangeDetails) => {
if (reason === 'input-change' || reason === 'item-select') {
flushSync(() => {
if (isValidNewOption(inputValue)) {
upsert(NEW_OPTION_VALUE, api.create(inputValue))
} else if (inputValue.trim().length === 0) {
remove(NEW_OPTION_VALUE)
}
})
filter(inputValue)
}
setInputValue(inputValue)
}
const handleOpenChange = ({ reason }: ComboboxOpenChangeDetails) => {
if (reason === 'trigger-click') {
filter('')
}
}
const handleValueChange = ({ value }: ComboboxValueChangeDetails) => {
setSelectedValue(api.replaceValue(value, inputValue))
if (value.includes(NEW_OPTION_VALUE)) {
console.log('New Option Created', inputValue)
update(NEW_OPTION_VALUE, api.getNewOptionData(inputValue))
}
}
return (
<Box w="1/2">
<Combobox
allowCustomValue
collection={collection}
label="Select Relative"
onOpenChange={handleOpenChange}
onInputValueChange={handleInputChange}
onValueChange={handleValueChange}
placeholder="Choose option"
value={selectedValue}
>
<For
each={collection.items}
fallback={
<VStack paddingBlock="6" w="full">
<Text textAlign="center" textStyle="label-sm">
No results found
</Text>
</VStack>
}
>
{(item) => (
<ComboItemWithIndicator key={item.value} item={item}>
<Show
when={api.isNew(item.value)}
fallback={<ComboItemText>{item.label}</ComboItemText>}
>
{() => (
<ComboItemText textStyle="label-sm">
+ Create "{item.label}"
</ComboItemText>
)}
</Show>
<Show when={item.__new__}>
{() => (
<Tag palette="success" usage="outlined">
New <Corn />
</Tag>
)}
</Show>
</ComboItemWithIndicator>
)}
</For>
</Combobox>
</Box>
)
}
On this page
No results found