Preact Multi-select Combo Box component
npm install preact-comboboxA Preact multi-select/single-select combobox component.
Interactive examples - https://munawwar.github.io/preact-combobox/example/example.html
(The code for the examples is in the git repo example/ directory).
``bashnpm add preact first`
npm add preact-combobox @popperjs/core
`jsx
import PreactCombobox from "preact-combobox";
import "preact-combobox/dist/esm/PreactCombobox.css";
const options = [
{ label: "Option 1", value: "option-1" },
{ label: "Option 2", value: "option-2", icon: "🔍" }, // Optional icon
];
function App() {
const [values, setValues] = useState(["option-1"]);
// For single-select mode
const [singleValue, setSingleValue] = useState("option-1");
return (
<>
{/ Multi-select (default) /}
allowedOptions={options}
value={values}
onChange={setValues}
/>
{/ Single-select /}
multiple={false}
allowedOptions={options}
value={singleValue}
onChange={setSingleValue}
/>
>
);
}
`
If you have a build tool:
`js`
// In your main JS/TS entry file
import 'preact-combobox/dist/esm/PreactCombobox.css';
Or download the CSS file from src/PreactCombobox.css and use it directly. Or you could use a CDN like esm.sh or unpkg.com if you wish:`css`
@import 'https://esm.sh/preact-combobox/dist/esm/PreactCombobox.css';
/ Or use a CSS layer in modern browser to make overriding design easier /
@import 'https://esm.sh/preact-combobox/dist/esm/PreactCombobox.css' layer(components);
/ Or /
@layer components {
@import 'https://esm.sh/preact-combobox/dist/esm/PreactCombobox.css';
}
You can customize the component's appearance by overriding CSS variables. The component uses CSS variables for theming:
`css`
:root {
/ Light theme overrides /
--preact-combobox-light-bg: #fff;
--preact-combobox-light-text: #333;
--preact-combobox-light-border: #ddd;
/ See PreactCombobox.css for all available variables /
}
`html`
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| id | string | (required) | Component ID |className
| | string | "" | Additional CSS class |allowedOptions
| | Option[] \| Function | (required) | Array of options or function for lazy-loading |onChange
| | Function | (required) | Callback for value change |value
| | string[] \| string | [] or "" | Selected value(s) |multiple
| | boolean | true | Enable multi-select mode |allowFreeText
| | boolean | false | Allow custom values |language
| | string | "en" | BCP 47 language code for text matching and word segmentation (passed directly to Intl.Segmenter and Intl.Collator) |placeholder
| | string | "" | Input placeholder text shown when no selections are made |disabled
| | boolean | false | Disable the combobox |required
| | boolean | false | Mark as required |showClearButton
| | boolean | true | Show clear button for selections |name
| | string | undefined | Input name (for forms) |theme
| | "light" \| "dark" \| "system" | "system" | Theme for the component |showValue
| | boolean | false | Show value alongside label |optionRenderer
| | Function | defaultOptionRenderer | Custom option renderer |optionIconRenderer
| | Function | defaultOptionIconRenderer | Custom icon renderer for options |translations
| | Translations | defaultEnglishTranslations | Localization strings |formSubmitCompatible
| | boolean | false | Include hidden form elements for submission |portal
| | HTMLElement | document.body | Element to render dropdown into |rootElementProps
| | Object | {} | Props for root element |inputProps
| | Object | {} | Props for input element |selectElementProps
| | Object | {} | Props for hidden select element |tray
| | boolean \| "auto" | "auto" | Enable mobile tray mode - true/false or 'auto' for media query detection |trayBreakpoint
| | string | "768px" | CSS breakpoint for auto tray mode (e.g., '768px', '50rem') |trayLabel
| | string | undefined | Label text for the tray header (auto-detects from associated label if not provided) |warningIcon
| | VNode | (default icon) | Custom warning icon |tickIcon
| | VNode | (default icon) | Custom tick icon for selected options |chevronIcon
| | VNode | (default icon) | Custom dropdown icon |loadingRenderer
| | (text: string) => VNode\|string | (default text) | Custom loading indicator |
`jsx/api/search?q=${query}&limit=${limit}
// Simulate API call for options
const fetchOptions = async (query, limit, currentSelections, signal) => {
// Return Promise, { signal });
return response.json();
};
function LazyExample() {
const [values, setValues] = useState([]);
return (
allowedOptions={fetchOptions}
value={values}
onChange={setValues}
placeholder="Type to search..."
/>
);
}
`
`jsx
const arabicTranslations = {
searchPlaceholder: "بحث...",
noOptionsFound: "لم يتم العثور على خيارات",
loadingOptions: "جاري التحميل...",
// Additional translations...
};
function RTLExample() {
const [values, setValues] = useState([]);
return (
$3
`jsx
`$3
`jsx
import { h } from "preact";// Here is how to customize the looks of dropdown options
// This is the exact defaultOptionRenderer from PreactCombobox.jsx
import { matchSlicesToNodes } from 'preact-combobox';
/* @type {import('preact-combobox').OptionTransformFunction} /
function defaultOptionRenderer({
option,
isSelected,
isInvalid,
showValue,
warningIcon,
optionIconRenderer,
}) {
const isLabelSameAsValue = option.value === option.label;
/**
* @param {(VNode|string)[]} labelNodes
* @param {(VNode|string)[]} valueNodes
* @returns {VNode}
*/
const getLabel = (labelNodes, valueNodes) => (
<>
{optionIconRenderer?.(option, false)}
{labelNodes}
{isLabelSameAsValue || !showValue ? null : (
)}
>
);
const { label, value, matched, matchSlices } = option;
let labelElement;
if (matched === "label" || (matched === "value" && value === label)) {
const labelNodes = matchSlicesToNodes(matchSlices, label);
labelElement = getLabel(labelNodes, [value]);
} else if (matched === "value") {
const valueNodes = matchSlicesToNodes(matchSlices, value);
labelElement = getLabel([label], valueNodes);
} else {
// if matched === "none"
labelElement = getLabel([label], [value]);
}
return (
<>
className={
PreactCombobox-optionCheckbox ${}
>
{isSelected && }
{labelElement}
{isInvalid && warningIcon}
>
);
}// Here is how to customize the looks of the icon rendered before the option label text and input field
/* @type {NonNullable} /
const customIconRenderer = (option, isInput) => {
// Commented out code is the exact default icon renderer from PreactCombobox.jsx
// return option.icon ? (
//
// ) : null;
// But let me show you what else you could do:
const category = option.value.split('-')[0];
// or you could pass option.category in allowedOptions list and then do:
// const { category } = option;
// Different icon size when shown in input vs dropdown
const size = isInput ? 16 : 20;
// Icon mapping based on category
const icons = {
fruit:
,
vegetable:
,
other: 
};
return (
custom-icon category-${category}}>
{icons[category] || icons.other}
);
};
// Here is how to customize the warning icon (static, not a function)
const customWarningIcon =
;
// Here is how to customize the tick icon for selected options (static, not a function)
const customTickIcon =
;
// Here is how to customize the dropdown chevron (static, not a function)
const customChevronIcon = 
// Here is how to customize the loading indicator
/* @type {NonNullable} /
const customLoadingRenderer = (text) => (
{text}
);function CustomRenderingExample() {
const [values, setValues] = useState([]);
const options = [
{ label: "Apple", value: "fruit-apple", description: "Red and sweet" },
{ label: "Banana", value: "fruit-banana", description: "Yellow and curved" },
{ label: "Carrot", value: "vegetable-carrot", description: "Orange and crunchy" },
{ label: "Broccoli", value: "vegetable-broccoli", description: "Green and healthy" },
{ label: "Other item", value: "other-item", description: "Miscellaneous" }
];
return (
id="custom-rendering-example"
allowedOptions={options}
value={values}
onChange={setValues}
optionRenderer={defaultOptionRenderer}
optionIconRenderer={customIconRenderer}
warningIcon={customWarningIcon}
tickIcon={customTickIcon}
chevronIcon={customChevronIcon}
loadingRenderer={customLoadingRenderer}
/>
);
}
`Option Object Format
`js
{
label: "Display Text", // Required: text shown in dropdown
value: "option-value", // Required: unique identifier
icon: "🔍", // Optional: icon (string or VNode)
disabled: true, // Optional: whether option is disabled and cannot be selected
divider: true // Optional: show divider line below option (only when search is empty)
}
`About Accessibility
Some attempt has been made to make the component accessible in the following dimensions:
- Keyboard navigation
- Basic screen reader test only on Orca (v46.1). Help me out to test on more software and platforms.
- Default colors for light mode theme (passes WCAG and APCA contrast)
Run Demo
`bash
npm run dev
open http://localhost:3050/example/example.html in a browser
``