Skip to content
Dethink Components

ComponentsCombobox

Combobox

Let users search a list and choose an option.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import { Combobox, ComboboxItem } from "@dethink/components";

Try the examples, then open the code to use them in your app. Type to filter, use the arrow keys to move through matches, and Enter to select.

Basic

Static options filter against the typed text automatically.

Filtering matches as you type.
Show sourceexamples/combobox/basic.tsx
examples/combobox/basic.tsx
"use client";

import { Combobox, ComboboxItem } from "@dethink/components";

export function ComboboxBasic() {
  return (
    <div className="mx-auto max-w-xs">
      <Combobox
        label="Assignee"
        placeholder="Type to filter people"
        description="Filtering matches as you type."
        name="assignee"
      >
        <ComboboxItem value="amara">Amara Okafor</ComboboxItem>
        <ComboboxItem value="jonas">Jonas Weber</ComboboxItem>
        <ComboboxItem value="mei">Mei Tanaka</ComboboxItem>
        <ComboboxItem value="ravi">Ravi Sharma</ComboboxItem>
        <ComboboxItem value="sofia">Sofía Delgado</ComboboxItem>
      </Combobox>
    </div>
  );
}

Rich options

Dynamic items with rich option layouts; textValue keeps filtering and announcement working, and menuTrigger opens the list on focus.

Show sourceexamples/combobox/rich-options.tsx
examples/combobox/rich-options.tsx
"use client";

import { Combobox, ComboboxItem } from "@dethink/components";

const repositories = [
  { value: "dethink/components", stars: "2.4k", language: "TypeScript" },
  { value: "dethink/registry", stars: "830", language: "TypeScript" },
  { value: "dethink/docs", stars: "410", language: "MDX" },
  { value: "dethink/examples", stars: "220", language: "TypeScript" },
];

export function ComboboxRichOptions() {
  return (
    <div className="mx-auto max-w-sm">
      <Combobox
        label="Repository"
        placeholder="Search repositories"
        items={repositories}
        menuTrigger="focus"
      >
        {(repo) => (
          <ComboboxItem
            key={repo.value}
            value={repo.value}
            textValue={repo.value}
          >
            <span className="flex w-full items-baseline justify-between gap-3">
              <span className="font-mono text-sm">{repo.value}</span>
              <span className="text-muted-foreground text-xs">
                {repo.language} · ★ {repo.stars}
              </span>
            </span>
          </ComboboxItem>
        )}
      </Combobox>
    </div>
  );
}

Custom values

allowsCustomValue keeps free text that matches no option, and formValue submits the typed text instead of an option key — the classic create-a-new-tag pattern.

Pick an existing label or type a new one.

Will apply: bug

Show sourceexamples/combobox/custom-value.tsx
examples/combobox/custom-value.tsx
"use client";

import { useState } from "react";
import { Combobox, ComboboxItem } from "@dethink/components";

export function ComboboxCustomValue() {
  const [label, setLabel] = useState<string | null>("bug");

  return (
    <div className="mx-auto max-w-xs space-y-2">
      <Combobox
        label="Label"
        description="Pick an existing label or type a new one."
        allowsCustomValue
        formValue="text"
        defaultInputValue="bug"
        onValueChange={setLabel}
        onInputValueChange={(text) => setLabel(text || null)}
      >
        <ComboboxItem value="bug">bug</ComboboxItem>
        <ComboboxItem value="enhancement">enhancement</ComboboxItem>
        <ComboboxItem value="documentation">documentation</ComboboxItem>
        <ComboboxItem value="good-first-issue">good first issue</ComboboxItem>
      </Combobox>
      <p className="text-muted-foreground text-sm">
        Will apply: {label ?? "nothing"}
      </p>
    </div>
  );
}

Form states

Required, disabled, read-only, and invalid with an error message.

You do not have access to this environment.
Show sourceexamples/combobox/states.tsx
examples/combobox/states.tsx
"use client";

import { Combobox, ComboboxItem } from "@dethink/components";

const environments = [
  { value: "production", label: "Production" },
  { value: "staging", label: "Staging" },
  { value: "sandbox", label: "Sandbox" },
];

export function ComboboxStates() {
  return (
    <div className="mx-auto grid max-w-md gap-5 sm:grid-cols-2">
      <Combobox label="Required" required items={environments}>
        {(item) => (
          <ComboboxItem key={item.value} value={item.value}>
            {item.label}
          </ComboboxItem>
        )}
      </Combobox>
      <Combobox
        label="Disabled"
        disabled
        defaultValue="staging"
        items={environments}
      >
        {(item) => (
          <ComboboxItem key={item.value} value={item.value}>
            {item.label}
          </ComboboxItem>
        )}
      </Combobox>
      <Combobox
        label="Read-only"
        readOnly
        defaultValue="production"
        items={environments}
      >
        {(item) => (
          <ComboboxItem key={item.value} value={item.value}>
            {item.label}
          </ComboboxItem>
        )}
      </Combobox>
      <Combobox
        label="Invalid"
        invalid
        errorMessage="You do not have access to this environment."
        defaultValue="production"
        items={environments}
      >
        {(item) => (
          <ComboboxItem key={item.value} value={item.value}>
            {item.label}
          </ComboboxItem>
        )}
      </Combobox>
    </div>
  );
}

Examples that combine components for common tasks.

Command palette

A command palette is a Combobox wearing different content: grouped commands with shortcut hints, selection running the command, and the input resetting for the next run. A live region announces what ran.

No command run yet.

Show sourceexamples/combobox/recipe-command-palette.tsx
examples/combobox/recipe-command-palette.tsx
"use client";

import { useState } from "react";
import { Combobox, ComboboxItem } from "@dethink/components";

const commands = [
  {
    value: "new-project",
    label: "Create new project…",
    shortcut: "⌘N",
    group: "Actions",
  },
  {
    value: "invite",
    label: "Invite teammate…",
    shortcut: "⌘I",
    group: "Actions",
  },
  {
    value: "toggle-theme",
    label: "Toggle color scheme",
    shortcut: "⌘⇧L",
    group: "Actions",
  },
  {
    value: "goto-dashboard",
    label: "Go to dashboard",
    shortcut: "G D",
    group: "Navigate",
  },
  {
    value: "goto-billing",
    label: "Go to billing",
    shortcut: "G B",
    group: "Navigate",
  },
  {
    value: "goto-settings",
    label: "Go to settings",
    shortcut: "G S",
    group: "Navigate",
  },
];

/**
 * A command palette is just a Combobox wearing different content: filtering,
 * keyboard navigation, and announcement come built in. Selecting a command
 * runs it and resets the input so the palette is ready for the next run.
 */
export function ComboboxRecipeCommandPalette() {
  const [inputValue, setInputValue] = useState("");
  const [lastCommand, setLastCommand] = useState<string | null>(null);

  return (
    <div className="mx-auto max-w-sm space-y-3">
      <Combobox
        aria-label="Command palette"
        placeholder="Type a command or search…"
        menuTrigger="focus"
        items={commands}
        inputValue={inputValue}
        onInputValueChange={setInputValue}
        value={null}
        onValueChange={(value) => {
          if (!value) {
            return;
          }
          const command = commands.find((entry) => entry.value === value);
          setLastCommand(command?.label ?? value);
          setInputValue("");
        }}
      >
        {(command) => (
          <ComboboxItem
            key={command.value}
            value={command.value}
            textValue={command.label}
          >
            <span className="flex w-full items-center justify-between gap-4">
              <span className="flex items-baseline gap-2">
                <span className="text-muted-foreground text-xs tracking-wide uppercase">
                  {command.group}
                </span>
                {command.label}
              </span>
              <kbd className="border-border bg-muted text-muted-foreground rounded border px-1.5 font-mono text-xs">
                {command.shortcut}
              </kbd>
            </span>
          </ComboboxItem>
        )}
      </Combobox>
      <p aria-live="polite" className="text-muted-foreground text-sm">
        {lastCommand ? `Ran: ${lastCommand}` : "No command run yet."}
      </p>
    </div>
  );
}

Combobox owns its field anatomy — pass a label instead of wrapping it in an external one. ComboboxItem takes the same value/textValue/disabled props as SelectItem.

Combobox props
PropWhat it doesDefault
valuestring | nullThe selected value. Use with onValueChange when your app manages the selection.Not set
defaultValuestring | nullThe starting selection when the component manages its own state.Not set
onValueChange(value: string | null) => voidCalled with the selected value, or null when the selection is cleared.Not set
inputValue / defaultInputValue / onInputValueChangestring / string / (text) => voidUse inputValue with onInputValueChange to manage the search text, or defaultInputValue to set its starting text.Not set
childrenComboboxItem nodes | (item) => ComboboxItemAdd ComboboxItem elements, or a function that renders each entry in items.Not set
items / defaultItemsIterable<ComboboxItemData>The available options. Use defaultItems for built-in filtering, or items when your app filters the list.Not set
menuTrigger"input" | "focus" | "manual"Sets when the list opens: on typing, on focus, or only from the button."input"
allowsCustomValuebooleanKeeps free text that matches no option as the value.false
formValue"key" | "text"Chooses whether the form submits the selected option value or the typed text."key"
disabledKeysIterable<string>The values of options users cannot select.Not set
label / description / errorMessageReactNodeThe field label, help text, and error message.Not set
placeholderstringInput placeholder before any text is typed.Not set
controlSize"sm" | "md" | "lg"Sets the input height and text size."md"
required / disabled / readOnly / invalidbooleanMakes the field required, disabled, read-only, or invalid.false
namestringThe field name used when submitting a form.Not set