Skip to content
cennge-ui
GitHubv0.3.1, changelog

Combobox

A text field with a list under it: type to filter, pick one value or many.

reacttypescript
  1. Type to filter; the list opens under the field.
  2. Arrows move the active option, Enter picks it, Escape closes.
  3. In multiple mode values become tags; Backspace removes the last.
Live: the component itself, not a picture of it.

Field marks

Import
import { Combobox } from "cennge-ui";
Size
6.8 KB gzip for this import, its dependencies aside
Pattern
WAI-ARIA combobox with a listbox popup
Keyboard
Arrow up Arrow down HomeEndEnter Esc
State
Controlled with value and onValueChange, or uncontrolled
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Virtualizes
Past 100 options, by itself
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: combobox, dialog-scroll

When to use#

Reach for a Combobox when the list is long enough that people would rather type than scroll. It filters as you type, virtualizes itself past 100 options, and with isClearable one click returns it to the placeholder.

Similar#

  • Select is a fixed short list with nothing to type.
  • TagsInput makes tags from any free text; a Combobox with selectionMode="multiple" only takes tags from its list.

Pick one repository#

value is the chosen item's value, or null when nothing is chosen. isClearable adds a button that sets it back to null.

basic.tsx
import { useState } from "react";
import { Combobox } from "cennge-ui";

const repositories = [
  { value: "cennge-ui", label: "cennge-ui", description: "React UI library" },
  { value: "ai-niche-scout", label: "ai-niche-scout" },
  { value: "payload-cache", label: "payload-cache" },
];

export default function RepositoryPicker() {
  const [repository, setRepository] = useState<string | null>(null);
  return (
    <Combobox
      label="Repository"
      placeholder="Pick a repository"
      items={repositories}
      value={repository}
      onValueChange={setRepository}
      isClearable
      className="w-full max-w-80"
    />
  );
}

A thousand options#

Past 100 items the list renders only the rows in view, so typing stays instant. The active option is always in the DOM, so screen readers follow it.

long-list.tsx
import { Combobox } from "cennge-ui";

// 1,000 options: past 100 the list renders only the rows in view.
const releases = Array.from({ length: 1000 }, (_, index) => {
  const version = `0.${Math.floor(index / 100)}.${index % 100}`;
  return { value: version, label: `cennge-ui@${version}` };
});

export default function ReleasePicker() {
  return <Combobox label="Release" placeholder="Type a version, e.g. 0.3" items={releases} className="w-full max-w-80" />;
}

Props#

PropTypeDefault
selectionMode
"multiple" | "single"
"single"
values
string[]Only with selectionMode="multiple"
–
defaultValues
string[]Only with selectionMode="multiple"
–
onValuesChange
(values: string[]) => voidOnly with selectionMode="multiple"
–
itemsrequired
ComboboxItem[]
–
label
React.ReactNode
–
placeholder
string
–
description
React.ReactNode
–
errorMessage
React.ReactNode
–
emptyContent
React.ReactNodeShown when nothing matches the query.
"No results"
filter
(item: ComboboxItem, query: string) => booleanCustom matching. Default: case-insensitive "contains" on the label.
–
onInputChange
(query: string) => void
–
isClearable
boolean
–
isInvalid
boolean
–
isRequired
boolean
–
isDisabled
boolean
–
variant
"bordered""flat""faded""underlined"
"flat"
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
radius
"none""sm""md""lg""full"
"md"
startContent
React.ReactNode
–
name
stringForm field name; selected values are submitted as hidden inputs.
–
id
string
–
className
string
–
classNames
Partial<Record<ComboboxSlots, string>>
–
aria-label
string
–
value
string | nullOnly with selectionMode="single"
–
defaultValue
string | nullOnly with selectionMode="single"
–
onValueChange
(value: string | null) => voidOnly with selectionMode="single"
–