forms-creatable-multi-select
v0.1.0
Published
A polished, accessible, creatable multi-select for React with async search, keyboard navigation, and themeable UI.
Maintainers
Readme
forms-creatable-multi-select
A polished, accessible, and highly customizable creatable multi-select for React. It works for both JavaScript and TypeScript projects, supports async search, lets users create missing options on the fly, and ships with npm-friendly build metadata.
Highlights
- Multi-select with removable tags
- Create-new flow with sync or async handlers
- Async option loading with abort-aware search callbacks
- Keyboard navigation and sensible accessibility defaults
- Themeable via CSS variables
- Customizable via
classNames,styles,renderOption, andrenderTag - Hidden input rendering for plain HTML form submission
- Packaged for npm with ESM, CJS, CSS, and TypeScript declarations
Install
npm install forms-creatable-multi-selectPeer dependencies:
react >= 18react-dom >= 18
Most modern bundlers will pick up the emitted CSS automatically from the package entry. If your setup wants it explicitly, import the stylesheet yourself:
import "forms-creatable-multi-select/styles.css";Quick start (TypeScript)
import { useState } from "react";
import {
CreatableMultiSelect,
type CreatableOption,
} from "forms-creatable-multi-select";
const topicOptions: CreatableOption[] = [
{ value: "react", label: "React" },
{ value: "typescript", label: "TypeScript" },
{ value: "laravel", label: "Laravel" },
];
export function TopicPicker() {
const [topics, setTopics] = useState<readonly CreatableOption[]>([]);
return (
<CreatableMultiSelect
label="Topics"
value={topics}
onChange={setTopics}
options={topicOptions}
onCreateOption={async (inputValue) => ({
value: inputValue.toLowerCase(),
label: inputValue,
__isNew__: true,
})}
helperText="Select existing topics or create your own."
/>
);
}Quick start (JavaScript)
import { useState } from "react";
import { CreatableMultiSelect } from "forms-creatable-multi-select";
const options = [
{ value: "critical", label: "Critical" },
{ value: "nice-to-have", label: "Nice to have" },
];
export default function PriorityPicker() {
const [value, setValue] = useState([]);
return (
<CreatableMultiSelect
label="Priority labels"
value={value}
onChange={setValue}
options={options}
placeholder="Pick labels or create one"
/>
);
}Async loading and create flow
const loadOptions = async (query: string, signal: AbortSignal) => {
const response = await fetch(`/api/tags?q=${encodeURIComponent(query)}`, {
signal,
});
const data = await response.json();
return data.map((tag: { id: string; name: string }) => ({
value: tag.id,
label: tag.name,
}));
};
const createTag = async (inputValue: string) => {
const response = await fetch("/api/tags", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: inputValue }),
});
const tag = await response.json();
return {
value: tag.id,
label: tag.name,
};
};
<CreatableMultiSelect
label="Tags"
value={tags}
onChange={setTags}
options={[]}
loadOptions={loadOptions}
onCreateOption={createTag}
loadDebounceMs={200}
/>;Customization surfaces
Theme variables
Use the theme prop to override the built-in CSS variables without replacing the whole component style system.
<CreatableMultiSelect
theme={{
accent: "#7c3aed",
accentSoft: "rgba(124, 58, 237, 0.15)",
tagBackground: "#f5f3ff",
tagText: "#5b21b6",
radius: "28px",
}}
{...props}
/>Slot classes and inline styles
<CreatableMultiSelect
classNames={{
control: "my-control",
option: "my-option",
footer: "my-footer",
}}
styles={{
control: { minHeight: "4rem" },
option: { borderRadius: "18px" },
}}
{...props}
/>Custom renderers
<CreatableMultiSelect
renderOption={(option, state) => (
<div className="my-option-row">
<strong>{option.label}</strong>
<span>{state.isSelected ? "Selected" : option.description}</span>
</div>
)}
renderTag={(option, state) => (
<span className="my-chip">
{option.label}
<button type="button" onClick={state.onRemove}>
×
</button>
</span>
)}
{...props}
/>Core props
| Prop | Type | Notes |
| --- | --- | --- |
| value | readonly CreatableOption[] | Controlled value |
| defaultValue | readonly CreatableOption[] | Uncontrolled initial value |
| options | readonly CreatableOption[] | Available choices |
| onChange | (value, meta) => void | Called after select, remove, clear, or create |
| onCreateOption | (input) => option \| Promise<option> | Return the created option to auto-select it |
| loadOptions | (query, signal) => Promise<option[]> | Async search hook |
| classNames | Partial<SlotClassNames> | Slot-level class overrides |
| styles | Partial<SlotStyles> | Slot-level inline styles |
| theme | Partial<ThemeVars> | CSS variable overrides |
| renderOption | (option, state) => ReactNode | Custom menu row rendering |
| renderTag | (option, state) => ReactNode | Custom selected-chip rendering |
| footer | ReactNode \| (context) => ReactNode | Bottom menu content |
| name | string | Renders hidden inputs for form posts |
Package scripts
From packages/creatable-multi-select:
npm run dev
npm run test
npm run typecheck
npm run build
npm run pack:preview
npm run publish:dry-runLocal demo
The package includes a Vite-powered demo app in src/demo/.
npm run devPublish checklist
- Update
name,version,license,repository, and author metadata inpackage.json. - Run typecheck, tests, and build.
- Inspect the tarball with
npm run pack:preview. - Validate publishing with
npm run publish:dry-run. - Publish with your preferred npm access and tag strategy.
Exported API
CreatableMultiSelect(default export and named export)defaultTheme- Public TypeScript types such as
CreatableOption,CreatableMultiSelectProps,ThemeVars, and related renderer metadata
Notes
- The package is framework-agnostic beyond React itself; no Tailwind dependency is required.
- The demo lives in the package so you can refine UI and public API together.
- If you use server rendering, load the component only in environments where standard DOM interactions are available.
