<datalist>

HTML Datalist — Live Demos

5 examples · native HTML

Basic autocomplete — and the display label / submit ID pattern

The left shows simple datalist. The right shows the "display country name, submit 2-letter code" pattern using a hidden input. Watch the badge and log.

Simple datalist
Type a letter — browser filters the list
Display label → submit ID
Picks show "United States" but submit "US"
visible:
hidden:
--:--:--Type in either field — watch the log

The hidden input pattern

<input type="text" list="countries" id="country-input"> <input type="hidden" name="country_code" id="country-code"> <datalist id="countries"> <option value="United States" data-id="US"> <option value="United Kingdom" data-id="GB"> </datalist> // On input: find matching option, copy data-id to hidden input input.addEventListener('input', () => { const match = Array.from(datalist.options) .find(o => o.value === input.value); hiddenInput.value = match?.dataset.id ?? ''; });

Detecting selection vs free typing

There is no native selection event on datalist. This demo uses the input event to compare the current value against all options in real time. The status box shows which case applies.

Start typing above…
--:--:--Type or select — watch detection in real time

The detection pattern

// No native 'select' event exists on datalist // Compare value against options on every input event input.addEventListener('input', () => { const isFromList = Array.from(datalist.options) .some(opt => opt.value === input.value); if (isFromList) { handleListSelection(input.value); } else { handleFreeText(input.value); } });

Dynamic options — debounce + AbortController

Fetches from a public REST API (jsonplaceholder) as you type. Waits 300ms after your last keystroke. Cancels in-flight requests on new input. Won't fetch until you type 2+ characters.

Fetching from JSONPlaceholder API · debounced 300ms · AbortController cancels stale requests
--:--:--Type 2+ characters to trigger debounced fetch

The complete production pattern

const MIN_CHARS = 2; const DEBOUNCE = 300; let timer, controller; input.addEventListener('input', () => { if (input.value.length < MIN_CHARS) { datalist.innerHTML = ''; controller?.abort(); // cancel if < min chars return; } clearTimeout(timer); timer = setTimeout(() => fetchOptions(input.value), DEBOUNCE); }); async function fetchOptions(query) { controller?.abort(); // cancel previous request controller = new AbortController(); const res = await fetch(url, { signal: controller.signal }); const data = await res.json(); datalist.innerHTML = data .map(item => `<option value="${item.name}">`) .join(''); }

datalist with range, color & time inputs

datalist works with non-text input types too. Range gets tick marks, color gets preset swatches (Chrome/Edge), time gets quick-pick suggestions. Browser support varies — test on your targets.

50
0%25%50%75%100%
#2563eb
--:--:--Interact with any input to see datalist values

Browser support for non-text datalist

Range ticks
Chrome ✓ Firefox ✓ Safari ✗ Edge ✓
Color swatches
Chrome ✓ Firefox ✗ Safari ✗ Edge ✓
Time quick-picks
Chrome ✓ Firefox ✓ Safari ✗ Edge ✓

Pattern enforcement — list-only selection

By default datalist allows free text. Combine pattern + setCustomValidity() to enforce only listed values. The :user-invalid border and CSS error message appear on incorrect input.

Please select a framework from the list Only React, Vue, Angular, Svelte, Solid are valid
--:--:--Try typing a custom value — pattern rejects it

Multi-value tag input using datalist

Pick multiple options from a datalist. Each selection becomes a removable tag. The value is synced to a hidden input for form submission. Press Backspace to remove the last tag.

Selected tags will be submitted as comma-separated values
Submitted value:
--:--:--Select tags from the datalist — each becomes a pill
Read the tutorial