npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

preact-combobox

v0.3.1

Published

Preact Multi-select Combo Box component

Readme

PreactCombobox

A Preact multi-select/single-select combobox component.

screenshot

Interactive examples - https://munawwar.github.io/preact-combobox/example/example.html

(The code for the examples is in the git repo example/ directory).

Installation

# `npm add preact` first
npm add preact-combobox @popperjs/core

Usage

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) */}
      <PreactCombobox
        id="my-combobox"
        allowedOptions={options}
        value={values}
        onChange={setValues}
      />

      {/* Single-select */}
      <PreactCombobox
        id="single-select"
        multiple={false}
        allowedOptions={options}
        value={singleValue}
        onChange={setSingleValue}
      />
    </>
  );
}

CSS

If you have a build tool:

// 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:

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

Customizing Styles

You can customize the component's appearance by overriding CSS variables. The component uses CSS variables for theming:

: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 */
}

Without build tools

<script type="importmap">
  {
    "imports": {
      "preact": "https://esm.sh/preact@10",
      "preact/hooks": "https://esm.sh/preact@10/hooks",
      "preact/jsx-runtime": "https://esm.sh/preact@10/jsx-runtime",
      "preact/compat": "https://esm.sh/preact@10/compat",
      "htm": "https://esm.sh/htm",
      "@popperjs/core": "https://esm.sh/@popperjs/core@2",
      "preact-combobox": "https://esm.sh/preact-combobox",
    }
  }
</script>

Component Props

| 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 | | maxPresentedOptions | number | 100 | Maximum number of options to show in the dropdown. For async options, this value is passed as the limit parameter to the fetcher function |

Advanced Features

Async/Lazy-loaded Options

// Simulate API call for options
const fetchOptions = async (query, limit, currentSelections, signal) => {
  // Return Promise<Option[]>
  const response = await fetch(`/api/search?q=${query}&limit=${limit}`, { signal });
  return response.json();
};

function LazyExample() {
  const [values, setValues] = useState([]);
  
  return (
    <PreactCombobox
      id="lazy-example"
      allowedOptions={fetchOptions}
      value={values}
      onChange={setValues}
      placeholder="Type to search..."
    />
  );
}

RTL Support and Translations (Experimental)

const arabicTranslations = {
  searchPlaceholder: "بحث...",
  noOptionsFound: "لم يتم العثور على خيارات",
  loadingOptions: "جاري التحميل...",
  // Additional translations...
};

function RTLExample() {
  const [values, setValues] = useState([]);
  
  return (
    <div dir="rtl">
      <PreactCombobox
        id="rtl-example"
        allowedOptions={options}
        value={values}
        onChange={setValues}
        language="ar"
        translations={arabicTranslations}
      />
    </div>
  );
}

Form Submission Compatible

<form onSubmit={handleSubmit}>
  <PreactCombobox
    id="form-example"
    allowedOptions={options}
    value={values}
    onChange={setValues}
    formSubmitCompatible={true}
    name="selected-options"
    // use true if you are doing a server-side render
    isServer={typeof self === "undefined"}
  />
  <button type="submit">Submit</button>
</form>

Customizing Rendering and Icons

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)}
      <span className="PreactCombobox-optionLabelFlex">
        <span>{labelNodes}</span>
        {isLabelSameAsValue || !showValue ? null : (
          <span className="PreactCombobox-optionValue" aria-hidden="true">
            ({valueNodes})
          </span>
        )}
      </span>
    </>
  );

  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 (
    <>
      <span
        className={`PreactCombobox-optionCheckbox ${
          isSelected ? "PreactCombobox-optionCheckbox--selected" : ""
        }`}
      >
        {isSelected && <span aria-hidden="true">✓</span>}
      </span>
      {labelElement}
      {isInvalid && warningIcon}
    </>
  );
}

// Here is how to customize the looks of the icon rendered before the option label text and input field
/** @type {NonNullable<import('preact-combobox').PreactComboboxProps['optionIconRenderer']>} */
const customIconRenderer = (option, isInput) => {
  // Commented out code is the exact default icon renderer from PreactCombobox.jsx
  // return option.icon ? (
  //   <span className="PreactCombobox-optionIcon" aria-hidden="true" role="img">
  //     {option.icon}
  //   </span>
  // ) : 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: <img width={size} height={size} src="./fruit.svg" />,
    vegetable: <img width={size} height={size} src="./vegetable.svg" />,
    other: <img width={size} height={size} src="./other.svg" />
  };
  
  return (
    <span className={`custom-icon category-${category}`}>
      {icons[category] || icons.other}
    </span>
  );
};

// Here is how to customize the warning icon (static, not a function)
const customWarningIcon = <img class="PreactCombobox-warningIcon" src="warning.svg" />;

// Here is how to customize the tick icon for selected options (static, not a function)
const customTickIcon = <img class="PreactCombobox-tickIcon" src="checkmark.svg" />;

// Here is how to customize the dropdown chevron (static, not a function)
const customChevronIcon = <img class="PreactCombobox-chevron" src="chevron" />

// Here is how to customize the loading indicator
/** @type {NonNullable<import('preact-combobox').PreactComboboxProps['loadingRenderer']>} */
const customLoadingRenderer = (text) => (
  <div className="custom-loading">
    <svg className="spinner" viewBox="0 0 24 24" width="16" height="16">
      <circle cx="12" cy="12" r="10" stroke="currentColor" fill="none" strokeWidth="2" />
    </svg>
    <span>{text}</span>
  </div>
);

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 (
    <PreactCombobox
      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

{
  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)
}

Advanced: Using OptionsListbox Directly

If you need to build a custom UX (e.g., a menu dropdown with an embedded search field), you can use the extracted OptionsListbox component and helper utilities directly. See OptionsListbox.md for props, ref methods, and available helper exports.

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

npm run dev
# open http://localhost:3050/example/example.html in a browser