Headless, searchable, creatable + editable select / combobox for React.
A logic-only combobox you style yourself. Built on Radix Popover with asChild
on every part, so it drops straight into shadcn/ui — bring your own
<Button>, <Input>, and icons. No CSS ships in the box.
- Searchable — built-in filtering (or supply your own).
- Creatable — inline "create new" flow.
- Editable — inline rename of existing options (the pencil).
- Single & multi-select —
multipleflipsvalueto an array; useItemIndicatorfor the left-side checkmark. - Optional delete — opt in by passing
onDelete. - Async-first — create / edit / delete callbacks may return Promises, with a shared
status/errorlifecycle. - Headless — a
useCreatableSelecthook andasChildcompound primitives. - RSC-safe — ships the
"use client"directive; ESM + CJS + types.
npm i react-creatable-select @radix-ui/react-popoverreact, react-dom, and @radix-ui/react-popover are peer dependencies.
<Select>— a batteries-included component with flat props (searchable/creatable/multiple/ …), à la react-select. Headless (no CSS ships) but renders complete default DOM. Start here.CreatableSelect.*primitives (+ theuseCreatableSelecthook) — the headless engine for full control over markup. Drop down when you outgrow<Select>.
import { Select } from "react-creatable-select";
// Your data shape — no need for {value,label}; map it with getters.
const cases = [{ id: "1", name: "Case 1" }, { id: "2", name: "Case 2" }];
<Select
options={cases}
value={value}
onChange={(v, option) => setValue(v)}
getOptionValue={(c) => c.id}
getOptionLabel={(c) => c.name}
searchable // show the search box + filter (default true)
creatable // show inline "create new" (default false)
multiple // multi-select; value becomes string[] (default false)
clearable // show a clear button (default false)
placeholder="Select case"
onCreate={(label) => api.createCase(label)} // sync or async → returns your option
onEdit={(opt, label) => api.renameCase(opt, label)} // presence enables the pencil
onDelete={(opt) => api.deleteCase(opt)} // presence enables delete
/>;No CSS ships. <Select> renders default DOM with stable rcs-* class hooks
and the same data-* attributes as the primitives — style off those, pass a
className (applied to the trigger) or a per-part classNames map, and override
the built-in icons via the icons prop. The list is managed for you: options
seeds the working list and create/edit/delete mutate a local copy (calling your
async callbacks for persistence); pass a new options reference to resync.
See SelectProps for the full surface.
import { CreatableSelect } from "react-creatable-select";
<CreatableSelect.Root
value={value}
onValueChange={setValue}
defaultOptions={[{ value: "1", label: "Case 1" }]}
onCreate={(label) => api.createCase(label)} // sync or async → returns the option
onEdit={(opt, label) => api.renameCase(opt, label)}
>
<CreatableSelect.Trigger asChild>
<Button variant="outline"><CreatableSelect.Value placeholder="Select case" /></Button>
</CreatableSelect.Trigger>
<CreatableSelect.Content>
<CreatableSelect.Search placeholder="Search" />
<CreatableSelect.CreateTrigger>+ Create New</CreatableSelect.CreateTrigger>
<CreatableSelect.CreateForm>
<CreatableSelect.CreateInput placeholder="New case" />
<CreatableSelect.CreateCommit>Create</CreatableSelect.CreateCommit>
<CreatableSelect.CreateCancel>✕</CreatableSelect.CreateCancel>
</CreatableSelect.CreateForm>
{/* `options` is the live, filtered list — reflects search and runtime-created options. */}
<CreatableSelect.List>
{(options) => (
<>
<CreatableSelect.Empty>No results.</CreatableSelect.Empty>
{options.map((o) => (
<CreatableSelect.Item key={o.value} value={o.value}>
<CreatableSelect.ItemIndicator><Check /></CreatableSelect.ItemIndicator>
<CreatableSelect.ItemLabel />
<CreatableSelect.ItemInput /> {/* shown while renaming */}
<CreatableSelect.ItemEditTrigger><Pencil /></CreatableSelect.ItemEditTrigger>
</CreatableSelect.Item>
))}
</>
)}
</CreatableSelect.List>
</CreatableSelect.Content>
</CreatableSelect.Root>A full Tailwind/shadcn version matching the mockup (single and multi) lives in
example/case-select.tsx.
<CreatableSelect.Root multiple value={values} onValueChange={setValues} ...>value / selected become arrays, closeOnSelect defaults to false, and
select toggles. Put <CreatableSelect.ItemIndicator> on the left of the item
to render a checkmark only for selected rows.
Delete is off until you provide onDelete. Once you do,
<CreatableSelect.ItemDeleteTrigger> renders (it returns null otherwise):
<CreatableSelect.Root onDelete={(opt) => api.deleteCase(opt)} ...>
...
<CreatableSelect.ItemDeleteTrigger><Trash /></CreatableSelect.ItemDeleteTrigger>Style purely off attributes — never reach into internals:
| Attribute | On | Meaning |
|---|---|---|
data-state="open|closed" |
Trigger | popover open state |
data-highlighted |
Item | keyboard/pointer highlight |
data-selected |
Item | option is selected |
data-editing |
Item | this row is being renamed |
data-state="checked|unchecked" |
ItemIndicator | selection (for gutter reservation) |
data-pending |
CreateInput / CreateCommit / ItemInput | async mutation in flight |
[data-highlighted] { background: var(--accent); }
[data-selected] { font-weight: 600; }Skip the primitives entirely and drive your own markup:
const select = useCreatableSelect({ options, onCreate });
<input {...select.getSearchProps()} />
{select.filtered.map((o) => <li {...select.getItemProps(o)}>{o.label}</li>)}See CreatableSelectController for the full surface (open, query,
filtered, selected, mode, status, commitCreate, commitEdit,
commitDelete, …).
MIT