Skip to content

ssunils/react-creatable-select

Repository files navigation

react-creatable-select

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-selectmultiple flips value to an array; use ItemIndicator for the left-side checkmark.
  • Optional delete — opt in by passing onDelete.
  • Async-first — create / edit / delete callbacks may return Promises, with a shared status / error lifecycle.
  • Headless — a useCreatableSelect hook and asChild compound primitives.
  • RSC-safe — ships the "use client" directive; ESM + CJS + types.

Install

npm i react-creatable-select @radix-ui/react-popover

react, react-dom, and @radix-ui/react-popover are peer dependencies.

Two ways to use it

  1. <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.
  2. CreatableSelect.* primitives (+ the useCreatableSelect hook) — the headless engine for full control over markup. Drop down when you outgrow <Select>.

Simplified API: <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.

Quick start (primitives)

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.

Multi-select

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

Optional delete

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>

Styling contract (data-*)

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; }

Hook-only usage

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, …).

License

MIT

Packages

 
 
 

Contributors