Logo VitNode
Documentation

Combobox

Pick one or many options from a searchable list, with groups, chips and async server search.

Preview

Usage

Options come from the z.enum. labels gives each value a readable name.

import { z } from 'zod'
import { AutoForm } from '@vitnode/core/components/form/auto-form'
import { AutoFormCombobox } from '@vitnode/core/components/form/fields/combobox'

const formSchema = z.object({
  category: z.enum(['general', 'support']),
})
<AutoForm
  formSchema={formSchema}
  fields={[
    {
      id: 'category',
      component: (props) => (
        <AutoFormCombobox
          {...props}
          label="Category"
          labels={[
            { value: 'general', label: 'General discussion' },
            { value: 'support', label: 'Help & support' },
          ]}
        />
      ),
    },
  ]}
/>

Groups and clear button

Give options a group to sort them under headings. Groups follow the order they first appear in labels; ungrouped options sit at the top. showClear adds an × that empties the field.

<AutoFormCombobox
  {...props}
  labels={[
    { group: 'Europe', label: 'Warsaw', value: 'europe-warsaw' },
    { group: 'Americas', label: 'New York', value: 'america-new-york' },
  ]}
  showClear
/>

fetchData loads options from your API as people type (debounced by 500ms). It needs a stable id and a queryKey for caching. Each picked value is a { value, label } object, not a string.

import { fetcher } from '@vitnode/core/tanstack/fetcher'

const formSchema = z.object({
  moderators: z
    .array(z.object({ value: z.string(), label: z.string() }))
    .min(1)
    .default([]),
})

;<AutoFormCombobox
  {...props}
  fetchData={async ({ search }) => {
    const res = await fetcher({
      plugin: '@acme/forum',
      method: 'get',
      module: 'members',
      path: '/',
      args: { query: { search } },
    })
    const data = await res.json()

    return data.edges.map((member) => ({
      label: member.name,
      value: member.id.toString(),
    }))
  }}
  id="moderators"
  multiple
  queryKey={['forum', 'members']}
  renderItem={(item) => <UserRow name={item.label} />}
  searchPlaceholder="Find a member..."
/>

renderItem customizes each async option, like adding an avatar.

Multiple

Add multiple and use an array in the schema. Picks become chips, and the box wraps when full. .min() and .max() on the array become the validation messages.

const formSchema = z.object({
  topics: z
    .array(z.enum(['news', 'guides', 'releases']))
    .min(1, 'Pick at least one topic')
    .max(3, 'Three topics is plenty'),
})
<AutoFormCombobox {...props} labels={topics} maxVisibleChips={3} multiple />
  • maxVisibleChips shows the first few chips plus "+N more". Every pick is still saved.
  • renderChip customizes each chip.
  • Backspace in an empty search removes the last chip. Escape with the list closed clears every pick, so mind that second Escape.
  • Reduced motion turns off the chip and chevron animations.

For a manual multi-select, wrap chips in ComboboxChips and anchor the popup to it:

const anchor = useComboboxAnchor()

;<Combobox items={frameworks} multiple>
  <ComboboxChips ref={anchor}>
    <ComboboxValue>
      {(values: string[]) => (
        <>
          {values.map((value) => (
            <ComboboxChip key={value}>{value}</ComboboxChip>
          ))}
          <ComboboxChipsInput placeholder="Add a framework" />
        </>
      )}
    </ComboboxValue>
  </ComboboxChips>
  <ComboboxContent anchor={anchor}>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Props

Prop

Type