Documentation

Combobox

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

Preview

Where your thread will be posted.

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

Used for scheduled posts and email digests.

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
/>

Results load from the server as you type.

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

Pick as many topics as you like - only the first three show up.

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