About this snippet

Start typing a country and a list of matches appears under the field, with the part you typed highlighted in each one. Arrow keys move through the list, Enter picks the highlighted match, Escape closes the list, and clicking a suggestion fills the field.

The suggestions come from a plain array filtered on every input event and capped at eight results. Each result is rebuilt as a list item with the matching characters wrapped in a mark element, which gives the highlight without fragile string splitting. Keyboard handling tracks an active index, wraps at either end, and mirrors it into aria-activedescendant so a screen reader announces the option the arrows have reached while focus stays in the input.

The markup follows the combobox pattern from the ARIA authoring practices. Swapping the array for a fetch call is a one line change in the filter function.

Size
4.9 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
24
<div class="search">
  <label for="country">Country of residence</label>
  <div class="combo">
    <input type="text" id="country" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="suggestions" autocomplete="off" placeholder="Start typing a country">
    <ul class="suggestions" id="suggestions" role="listbox" hidden></ul>
  </div>
  <p class="picked" id="picked" aria-live="polite"></p>
</div>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: #f1f5f9;
  color: #0f172a;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  min-height: 100vh;
  padding: 60px 20px;
}

.search {
  width: 100%;
  max-width: 420px;
}

.search label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  margin-bottom: 6px;
}

.combo {
  position: relative;
}

.combo input {
  width: 100%;
  font: inherit;
  padding: 13px 16px;
  border: 1.5px solid #cbd5e1;
  border-radius: 12px;
  background: #ffffff;
  outline: 0;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.combo input:focus {
  border-color: #2563eb;
  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.15);
}

.suggestions {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: #ffffff;
  border-radius: 12px;
  box-shadow: 0 20px 40px -12px rgba(15, 23, 42, 0.3);
  z-index: 10;
}

.suggestions li {
  padding: 10px 12px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9375rem;
}

.suggestions li:hover,
.suggestions li.active {
  background: #eff6ff;
}

.suggestions mark {
  background: transparent;
  color: #2563eb;
  font-weight: 700;
}

.suggestions .empty {
  color: #94a3b8;
  cursor: default;
}

.picked {
  margin: 14px 0 0;
  font-size: 0.875rem;
  color: #475569;
  min-height: 1.5em;
}
const countries = ["Argentina", "Australia", "Austria", "Belgium", "Brazil", "Canada", "Chile", "China", "Colombia", "Croatia", "Czechia", "Denmark", "Egypt", "Finland", "France", "Germany", "Greece", "Hungary", "Iceland", "India", "Indonesia", "Ireland", "Israel", "Italy", "Japan", "Kenya", "Malaysia", "Mexico", "Morocco", "Netherlands", "New Zealand", "Nigeria", "Norway", "Pakistan", "Peru", "Philippines", "Poland", "Portugal", "Romania", "Saudi Arabia", "Singapore", "South Africa", "South Korea", "Spain", "Sweden", "Switzerland", "Thailand", "Turkey", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Vietnam"];

const input = document.getElementById("country");
const list = document.getElementById("suggestions");
const picked = document.getElementById("picked");
let matches = [];
let active = -1;

function escapeRegExp(text) {
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function close() {
  list.hidden = true;
  list.innerHTML = "";
  input.setAttribute("aria-expanded", "false");
  input.removeAttribute("aria-activedescendant");
  active = -1;
}

function choose(value) {
  input.value = value;
  picked.textContent = "Selected: " + value;
  close();
}

function highlight() {
  const items = list.querySelectorAll("li");
  items.forEach(function(item, i) {
    item.classList.toggle("active", i === active);
    item.setAttribute("aria-selected", i === active);
  });
  if (active >= 0) {
    input.setAttribute("aria-activedescendant", items[active].id);
    items[active].scrollIntoView({ block: "nearest" });
  }
}

input.addEventListener("input", function() {
  const query = input.value.trim();
  if (!query) {
    close();
    return;
  }
  const pattern = new RegExp(escapeRegExp(query), "i");
  matches = countries.filter(function(name) {
    return pattern.test(name);
  }).slice(0, 8);
  list.innerHTML = "";
  if (!matches.length) {
    const li = document.createElement("li");
    li.className = "empty";
    li.textContent = "No matches";
    list.appendChild(li);
  }
  matches.forEach(function(name, i) {
    const li = document.createElement("li");
    li.id = "option-" + i;
    li.setAttribute("role", "option");
    li.innerHTML = name.replace(pattern, function(m) {
      return "<mark>" + m + "</mark>";
    });
    li.addEventListener("mousedown", function(e) {
      e.preventDefault();
      choose(name);
    });
    list.appendChild(li);
  });
  list.hidden = false;
  input.setAttribute("aria-expanded", "true");
  active = -1;
});

input.addEventListener("keydown", function(e) {
  if (list.hidden || !matches.length) return;
  if (e.key === "ArrowDown") {
    e.preventDefault();
    active = (active + 1) % matches.length;
    highlight();
  } else if (e.key === "ArrowUp") {
    e.preventDefault();
    active = (active - 1 + matches.length) % matches.length;
    highlight();
  } else if (e.key === "Enter" && active >= 0) {
    e.preventDefault();
    choose(matches[active]);
  } else if (e.key === "Escape") {
    close();
  }
});

input.addEventListener("blur", close);
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save