webdevpuneet.com

Choices.js Searchable Multi-Select — Free JS Snippet

Choices.js Searchable Multi-Select with Tags · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="cs-card">
  <label class="cs-label" for="csSkills">Skills for this role <span class="cs-hint">pick up to 5</span></label>
  <select id="csSkills" multiple>
    <optgroup label="Frontend">
      <option value="react" selected>React</option>
      <option value="vue">Vue</option>
      <option value="svelte">Svelte</option>
      <option value="css" selected>Modern CSS</option>
      <option value="a11y">Accessibility</option>
    </optgroup>
    <optgroup label="Backend">
      <option value="node">Node.js</option>
      <option value="go">Go</option>
      <option value="python">Python</option>
      <option value="sql">SQL</option>
      <option value="graphql">GraphQL</option>
    </optgroup>
    <optgroup label="Tooling">
      <option value="docker">Docker</option>
      <option value="ci">CI / CD</option>
      <option value="testing">Testing</option>
      <option value="figma">Figma</option>
    </optgroup>
  </select>
  <div class="cs-foot">
    <span class="cs-count" id="csCount">2 / 5 selected</span>
    <button type="button" class="cs-clear" id="csClear">Clear all</button>
  </div>
  <div class="cs-out" id="csOut" aria-live="polite"></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real <select multiple> stays as the source of truth and keeps submitting with the form
Searchable dropdown with live filtering and a custom no-results message
Removable tag chips with a themed remove button
Hard selection cap with a custom explanatory message
Grouped options via optgroup headings
Options keep authored order (shouldSort off) and results are capped for scanning
getValue(true) returns a plain string array for JSON or fetch bodies
Clear all uses removeActiveItems() so chips and select never drift apart

About this UI Snippet

Choices.js Searchable Multi-Select — HTML, CSS & JavaScript

Screenshot of the Choices.js Searchable Multi-Select with Tags snippet rendered live

A native <select multiple> is the right data model for a tag picker — it submits with the form, it is what screen readers and password managers already understand — but it is a terrible interface. Nobody wants to hold Ctrl and scroll a list box. Choices.js solves that by hiding the real select and rendering a searchable dropdown with removable chips on top of it, while keeping the original element as the single source of truth.

That "the select stays in sync" detail is what this snippet is built around. Nothing here reads values out of the widget's DOM; the change listener is attached to the original select, and choices.getValue(true) returns a plain array of value strings, ready for JSON or a form post. Because the underlying element is still a real select, the field keeps working in a normal form submission and degrades to the native control if the script fails to load.

The options are grouped with optgroup, so the dropdown shows Frontend, Backend and Tooling headings without any extra configuration. maxItemCount caps the selection at five, and maxItemText supplies the message shown once the cap is hit — without it users just see the list go quiet and assume it is broken. shouldSort is switched off so options keep the order you wrote them in rather than being re-sorted alphabetically, and searchResultLimit keeps the filtered list short enough to scan.

The styling overrides target Choices' own class names (choices__inner, choices__item, choices__list--dropdown) rather than fighting the library with wrappers, so the focus ring, chips and highlighted row all match one accent colour. The counter turns amber at the cap, which gives a second, non-modal cue that the limit has been reached. Clear all calls removeActiveItems(), the supported way to empty a multi-select — setting option.selected on the hidden element would leave the chips out of sync.

Step by step

How to Use

  1. 1
    Open the fieldClick into the input. The dropdown lists every skill grouped under Frontend, Backend and Tooling.
  2. 2
    Type to filterStart typing "sq" or "css" — the list narrows live and a no-results message appears if nothing matches.
  3. 3
    Add and remove chipsSelect options to add chips. Click the small × on a chip, or press Backspace in an empty field, to remove one.
  4. 4
    Hit the limitAdd five skills. The counter turns amber and the dropdown explains that no more can be added.
  5. 5
    Read the valueThe dark panel shows the array a form handler would receive. Use Clear all to reset it.

Real-world uses

Common Use Cases

Role and skill pickers
Attach skills, permissions or interests to a profile. Pair it with a tag input that creates new values when the list should be open-ended.
ADMIN
Admin filters
Let staff narrow a table by several statuses or owners at once without a wall of checkboxes.
SHOP
Product attribute selection
Choose sizes, colours or categories from a long, grouped list where a native multi-select would be unusable.
Learning progressive enhancement
A clear example of upgrading a native control instead of replacing it, so the form still works without JavaScript.

Got questions?

Frequently Asked Questions

Yes. Choices keeps the original select in sync, so the selected values post under the select's name exactly like a native multi-select.

Call choices.getValue(true) for an array of value strings, or omit true to get objects with value and label.

Set maxItemCount and provide maxItemText. Choices stops accepting items at the cap and shows your message.

shouldSort is disabled here on purpose so the authored order is kept. Remove that option if you want Choices to sort them.

Use choices.removeActiveItems(). Editing the hidden select's options directly would desynchronise the visible chips.

Yes — create the instance in an effect or mounted hook on a ref'd select, and call choices.destroy() in the cleanup.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page to convert the markup and styles. The behaviour comes from Choices.js, so in a framework project install it with npm install choices.js instead of the CDN tag, create it in useEffect / onMounted / ngAfterViewInit on the select element, and release it with destroy() when the component unmounts.