HTML datalist: suggestions for an input, free text still allowed

A <datalist> gives a text box a list of suggestions without locking the user into them. One attribute connects the two, and no JavaScript is needed for the basic version.

To add suggestions to a text box, write a <datalist> with an id, put <option> elements inside it, and give the <input> a list attribute with the same id.

The browser offers the options as the user types, and the user can still type something that is not on the list.

<input list="cities">
<datalist id="cities">
  <option value="Berlin">
  <option value="Paris">
</datalist>

Try it below. The left box has a datalist. The right one is a <select>, so you can feel the difference.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>datalist vs select</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
  .box { background: #fff; border-radius: 12px; padding: 14px; box-shadow: 0 3px 12px rgba(0, 0, 0, .08); }
  label { display: block; font-weight: 600; margin-bottom: 6px; }
  small { display: block; color: #5b6270; margin-bottom: 10px; }
  input, select { width: 100%; box-sizing: border-box; font: inherit; padding: 8px 10px; border: 1px solid #c9cdd4; border-radius: 8px; }
  output { display: block; margin-top: 10px; font-size: 14px; color: #0f5132; }
</style>
</head>
<body>
<div class="row">
  <div class="box">
    <label for="city">datalist</label>
    <small>Suggestions, but any text is allowed.</small>
    <!-- list="cities" points at the datalist's id -->
    <input id="city" list="cities" placeholder="Type a city">
    <datalist id="cities">
      <option value="Berlin">
      <option value="Lisbon">
      <option value="London">
      <option value="Paris">
      <option value="Seoul">
    </datalist>
    <output id="cityOut">Value: (empty)</output>
  </div>
  <div class="box">
    <label for="pick">select</label>
    <small>Only the options below can be chosen.</small>
    <select id="pick">
      <option>Berlin</option>
      <option>Lisbon</option>
      <option>London</option>
      <option>Paris</option>
      <option>Seoul</option>
    </select>
    <output id="pickOut">Value: Berlin</output>
  </div>
</div>

<script>
  const city = document.getElementById('city');
  const pick = document.getElementById('pick');
  // picking a suggestion fires the same input event as typing
  city.addEventListener('input', () => {
    document.getElementById('cityOut').textContent = 'Value: ' + (city.value || '(empty)');
  });
  pick.addEventListener('change', () => {
    document.getElementById('pickOut').textContent = 'Value: ' + pick.value;
  });
</script>
</body>
</html>
Left: datalist suggests cities and accepts any text. Right: select only allows its options.

The <datalist> element draws nothing by itself. It is hidden, and the browser uses its options only for inputs that point to it.

How list and id connect

The link is one string. The input's list attribute must equal the datalist's id, letter for letter and in the same case. The datalist can sit anywhere in the page, and several inputs can share one datalist.

list="cities" finds id="cities". One letter off and the box turns into a plain text field, with no error.
list="cities" finds id="cities". One letter off and the box turns into a plain text field, with no error.

If the link is broken, nothing warns you. A quick check in the browser console is input.list: it returns the datalist when the link works and null when it does not.

Browsers also differ in when the list opens. Some show all options when you click an empty field or press the Down arrow key; others wait for the first letter. How the list is filtered as you type is up to the browser as well.

datalist vs select: free text or fixed choice

Both show a list, but they answer different questions. A <select> says "choose one of these". A datalist says "here are some common answers, or write your own".

datalist suggests and then accepts anything. select only accepts what is listed.
datalist suggests and then accepts anything. select only accepts what is listed.
<input list> + <datalist> <select>
Other text allowed Yes No
Typing filters the list Yes No
Empty value possible Yes, unless required Only with an empty option
Style the open list with CSS No Only in limited ways
Good for Search, tags, cities, models Sizes, statuses, fixed sets

If you use a <select> and need a starting option, HTML dropdown default selected covers which option shows first and why.

Option value vs label: what goes into the box

An option can carry two pieces of text. The value attribute is what the input receives. A label attribute, or text inside the option, is extra description shown in the list.

The field always gets the value. The label is a hint in the list, and browsers show it differently.
The field always gets the value. The label is a hint in the list, and browsers show it differently.
<option value="LHR" label="London Heathrow">

Picking this option puts LHR in the box, and the form sends LHR.

Whether the label appears next to the value, or instead of it, differs between browsers. If you want people to see and type readable names, make the readable name the value and translate it to a code in your script.

Which input types take a list

The list attribute is not only for plain text. In Chromium, input.list finds the datalist for these types: text, search, url, tel, email, number, range, color, date, month, week, time and datetime-local. It is null for password, checkbox, radio, file, hidden and buttons.

What each type does with the list is less uniform:

  • range: Chromium draws a tick mark under the slider at each option value. Other browsers may not draw ticks, so treat them as a bonus.
  • color, date, time: the attribute is accepted, but how the suggestions appear varies by browser. Test them in the browsers your users have before relying on them.
  • email, url, search, tel: these behave like text, with suggestions shown as you type.

For what each type does without a list, see input types in HTML.

Fill a datalist from JavaScript and require a listed value

Long lists usually live in data, not in HTML. Create one <option> per item and append it to the datalist. The input picks up new options without any extra step.

Because a datalist accepts any text, a "must be from the list" rule is your job. setCustomValidity does it with the browser's own form checks: set a message when the value is not in the array, and an empty string when it is.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>datalist from an array</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  form { max-width: 420px; background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 3px 12px rgba(0, 0, 0, .08); }
  label { display: block; font-weight: 600; margin-bottom: 6px; }
  input { width: 100%; box-sizing: border-box; font: inherit; padding: 8px 10px; border: 1px solid #c9cdd4; border-radius: 8px; }
  input.bad { border-color: #c2410c; }
  button { margin-top: 12px; font: inherit; padding: 8px 16px; border: 0; border-radius: 8px; background: #2563eb; color: #fff; cursor: pointer; }
  #msg { margin-top: 12px; font-size: 14px; min-height: 1.4em; }
</style>
</head>
<body>
<form id="form">
  <label for="lang">Programming language</label>
  <input id="lang" name="lang" list="langs" placeholder="Start typing, e.g. Py">
  <datalist id="langs"></datalist>
  <button>Save</button>
  <div id="msg"></div>
</form>

<script>
  const langs = ['C', 'C#', 'C++', 'Go', 'Java', 'JavaScript', 'Kotlin', 'PHP', 'Python', 'Ruby', 'Rust', 'Swift', 'TypeScript'];
  const input = document.getElementById('lang');
  const list = document.getElementById('langs');
  const msg = document.getElementById('msg');

  // one <option> per array item
  for (const name of langs) {
    const opt = document.createElement('option');
    opt.value = name;
    list.append(opt);
  }

  // datalist accepts any text, so check the value ourselves
  function check() {
    const ok = langs.includes(input.value);
    input.setCustomValidity(ok ? '' : 'Pick a language from the list.');
    input.classList.toggle('bad', !ok && input.value !== '');
  }
  input.addEventListener('input', check);
  check();

  // runs only when the value is valid
  document.getElementById('form').addEventListener('submit', (e) => {
    e.preventDefault();  // demo: show the value instead of sending it
    msg.textContent = 'Saved: ' + new FormData(e.target).get('lang');
    msg.style.color = '#0f5132';
  });

  // runs instead of submit when the value is not in the list
  input.addEventListener('invalid', () => {
    msg.textContent = input.value ? '"' + input.value + '" is not in the list.' : 'Pick a language first.';
    msg.style.color = '#9a3412';
  });
</script>
</body>
</html>
Options built from an array. Type "Pyth" and press Save: blocked. Pick "Python": saved.
const ok = langs.includes(input.value);
input.setCustomValidity(ok ? '' : 'Pick a language from the list.');

While the message is set, the form will not submit and the browser shows the message near the field. The required attribute alone only checks that the box is not empty. More on these checks in HTML form validation.

In a search box, suggestions and results should come from the same data. This example builds the datalist from a product array, then filters product cards on every input event. Typing and picking a suggestion both fire that event, so one listener covers both.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Product search with datalist</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .search { display: flex; gap: 8px; max-width: 520px; }
  input { flex: 1; min-width: 0; font: inherit; padding: 10px 12px; border: 1px solid #c9cdd4; border-radius: 10px; }
  button { font: inherit; padding: 0 14px; border: 1px solid #c9cdd4; border-radius: 10px; background: #fff; cursor: pointer; }
  #count { margin: 12px 0 8px; font-size: 13px; color: #5b6270; }
  #results { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
  .item { background: #fff; border-radius: 10px; padding: 10px 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .07); border: 2px solid transparent; }
  .item.exact { border-color: #16a34a; }
  .item b { display: block; font-size: 14px; }
  .item span { font-size: 12px; color: #5b6270; }
  .item i { display: block; font-style: normal; margin-top: 4px; font-weight: 600; }
</style>
</head>
<body>
<div class="search">
  <input id="q" type="search" list="names" placeholder="Search products" aria-label="Search products">
  <button id="clear" type="button">Clear</button>
</div>
<datalist id="names"></datalist>
<div id="count"></div>
<div id="results"></div>

<script>
  const products = [
    { name: 'Desk lamp', cat: 'Lighting', price: 39 },
    { name: 'Floor lamp', cat: 'Lighting', price: 89 },
    { name: 'Standing desk', cat: 'Furniture', price: 420 },
    { name: 'Desk chair', cat: 'Furniture', price: 210 },
    { name: 'Monitor arm', cat: 'Accessories', price: 65 },
    { name: 'Cable tray', cat: 'Accessories', price: 24 },
    { name: 'Wool rug', cat: 'Textiles', price: 150 },
    { name: 'Linen curtain', cat: 'Textiles', price: 58 }
  ];
  const q = document.getElementById('q');
  const results = document.getElementById('results');

  // suggestions come from the same data as the results
  document.getElementById('names').append(...products.map((p) => {
    const o = document.createElement('option');
    o.value = p.name;   // goes into the box
    o.label = p.cat;    // extra hint; browsers differ in whether they show it
    return o;
  }));

  function render() {
    const text = q.value.trim().toLowerCase();
    const hits = products.filter((p) =>
      p.name.toLowerCase().includes(text) || p.cat.toLowerCase().includes(text));
    results.innerHTML = '';
    for (const p of hits) {
      const card = document.createElement('div');
      card.className = 'item' + (p.name.toLowerCase() === text ? ' exact' : '');
      card.innerHTML = '<b></b><span></span><i></i>';
      card.querySelector('b').textContent = p.name;
      card.querySelector('span').textContent = p.cat;
      card.querySelector('i').textContent = '$' + p.price;
      results.append(card);
    }
    document.getElementById('count').textContent = hits.length + ' of ' + products.length + ' products';
  }

  q.addEventListener('input', render);  // typing and picking a suggestion both land here
  document.getElementById('clear').addEventListener('click', () => { q.value = ''; render(); q.focus(); });
  render();
</script>
</body>
</html>
Type "desk", or pick a product from the suggestions. An exact name gets a green outline.
  • One source of truth: the products array feeds both the datalist and the results.
  • Label as a hint: each option has the category as its label; some browsers show it in the list.
  • Safe output: product names go in with textContent, not raw HTML strings.

For the basics of the box itself, such as labels and sizing, see text input in HTML.

When it does not work

What you see Cause Fix
No suggestions at all list does not match the datalist id Make them identical, same case
No suggestions on a password field password ignores list Use a type that accepts it
List appears only after typing The browser opens the list at its own time Hint in the placeholder, such as "Start typing"
The box shows a code, not the name The option value is the code; label is only a hint Put the readable text in value
Users submit text not on the list datalist never restricts input Check in JavaScript with setCustomValidity
CSS does not change the dropdown The browser draws the list Style the input, or build a custom list
No ticks on a range slider Tick marks are drawn by some browsers only Show the steps as labels under the slider

A datalist is easier to judge by typing into it than by looking at a screenshot. A screenshot shows the list frozen in one browser's style, and it cannot be typed into.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can type, pick suggestions and see the results themselves. If you change the options or the code later, the same link shows the new version.

Questions people ask

What is the difference between datalist and select?

A select only lets the user pick one of its options. An input with a datalist shows the options as suggestions, but the user can also type anything else. Use select for a fixed set of answers and datalist when the list is a shortcut, not a rule.

Can I style the datalist dropdown with CSS?

No. The suggestion list is drawn by the browser, and CSS has no standard way to reach it. You can style the input itself. For a fully styled list you need a custom list built with HTML and JavaScript.

How do I force the user to pick a value from the datalist?

The datalist itself never blocks other text. Check the value in JavaScript, for example on the input event, and call setCustomValidity with a message when the value is not in your list. The form then refuses to submit and shows that message.

Why is my datalist not showing any suggestions?

Most often the list attribute on the input does not exactly match the id of the datalist. Also check that the input type accepts a list: password, checkbox, radio, file and hidden inputs ignore it.

Does the form send the option value or the label?

The value. When the user picks a suggestion, its value attribute is written into the input, and that is what the form submits. The label is only extra text in the suggestion list.

Keep reading