Choices.js Remote-Loaded Options — Free JS Snippet

Choices.js Remote-Loaded Options with Stale-Response Guard · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

What's included

Features

Server-driven options — local filtering disabled with searchChoices: false
Per-request ids so out-of-order responses can never overwrite newer results
Live counters showing requests fired, applied and discarded
Debounced input and a minimum query length to spare the API
Disabled placeholder choices for loading, empty and prompt states
setChoices(..., true) replaces the list instead of appending duplicates
Drop-in seam: replace fakeApi with fetch and an AbortController

About this UI Snippet

Choices.js Remote-Loaded Options — HTML, CSS & JavaScript

Screenshot of the Choices.js Remote-Loaded Options with Stale-Response Guard snippet rendered live

Loading dropdown options from a server is easy to get almost right. You listen for input, call the API, and put the results in the list. It works in testing because the network is fast and predictable. On a real connection it breaks in a quiet, maddening way: you type "lon", then "lond", and the response for "lon" arrives after the response for "lond" and replaces the better results with the worse ones. The user sees a list that does not match what is in the box.

This snippet exists to show that failure and fix it. The simulated API deliberately makes shorter queries slower, so responses arrive out of order the moment you type quickly. Every request takes an incrementing id, and a global latest counter holds the id of the newest one. When a response comes back, it is applied only if its id still equals latest; otherwise it is dropped and counted as stale. The three counters under the field make this visible — you will see "stale discarded" climb as you type, and the list will still always show results for the current text.

On the Choices.js side, searchChoices is set to false. By default Choices filters its own list locally as you type, which is exactly wrong when the server is the source of truth. With it off, the widget only raises a search event carrying the current text, and the code decides what to load. Results are swapped in with setChoices(items, 'value', 'label', true) — the final true replaces the existing list instead of appending — after clearChoices() has emptied it.

Keystrokes are debounced by 140ms so typing "berlin" does not fire six requests, and inputs shorter than two characters skip the network entirely. A disabled placeholder choice doubles as the loading and empty state, so the dropdown never shows a confusing blank panel. Swap fakeApi for a real fetch call and pass an AbortController signal if you also want to cancel the in-flight request rather than just ignoring it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to swap the mock for a real fetch with AbortController cancellation, add keyboard-friendly result highlighting of the matched text, or cache recent queries.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Choices.js 10 single-select whose options are loaded from an async API as the user types.

Requirements:
- Set searchChoices: false so Choices does not filter locally, and listen for the 'search' event on the select.
- Debounce input by about 150ms and skip requests for queries shorter than two characters.
- Give every request an incrementing id; when a response arrives, apply it only if its id equals the latest id, otherwise discard it.
- Render results with setChoices(items, 'value', 'label', true) after clearChoices(), and use a disabled placeholder choice for loading, empty and prompt states.
- Show live counters for requests fired, applied and stale-discarded, and make the mock API slower for shorter queries so the race is reproducible.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="cr-card">
  <label class="cr-label" for="crCity">Search a city <span class="cr-hint">results come from a simulated API</span></label>
  <select id="crCity">
    <option value="Lisbon" selected>Lisbon</option>
  </select>
  <div class="cr-stats" aria-live="polite">
    <div><b id="crFired">0</b><span>requests fired</span></div>
    <div><b id="crApplied">0</b><span>applied</span></div>
    <div><b id="crStale">0</b><span>stale discarded</span></div>
  </div>
  <p class="cr-note">Type quickly — earlier requests are slower on purpose, so they finish after later ones. Only the newest response is allowed to update the list.</p>
</div>

Step by step

How to Use

  1. 1
    Open the dropdownClick the field. It prompts you to type at least two letters before anything is requested.
  2. 2
    Type slowlyType "par". After a short debounce a request fires, a "Searching..." row appears, and matching cities load.
  3. 3
    Type quicklyType "lon" then keep going to "london" in one burst. Watch "stale discarded" count up while the list stays correct.
  4. 4
    Read the countersFired minus applied equals discarded plus in-flight — proof that old responses were never allowed to win.
  5. 5
    Pick a citySelect a result to set it as the field value; Choices closes the list and shows your choice.

Real-world uses

Common Use Cases

Address and city lookups
Autocomplete places from a geocoding API. For a map-based variant see the Leaflet location picker.
ADMIN
User and record pickers
Search a directory with thousands of entries where loading everything up front would be far too heavy.
SHOP
Catalogue product search
Pick a product or SKU by typing, with results ranked by the server.
Learning async race conditions
A small, visible demonstration of why "latest request wins" matters for every type-ahead field.

Got questions?

Frequently Asked Questions

Choices would otherwise filter the currently loaded options locally, hiding server results that do not contain the typed text. Turning it off leaves filtering to your API.

A response to a request that has since been superseded by a newer one. Applying it would replace fresher results with older ones, so it is ignored.

No. Debouncing reduces requests but responses can still return out of order. The id check is what guarantees correctness.

Replace fakeApi with fetch(url).then(r => r.json()). Use an AbortController if you also want to cancel the older in-flight request.

Store the chosen value and label, then call setChoices with that single item marked selected before the user types anything.

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.