Live Search (List.js)

Fuzzy live search over a list of items using List.js. Supports searching by name and custom keyword fields, with a "no results" state and automatic result updates as the user types.

searchlist.jsfuzzyfilterjavascript

Setup — External Scripts

CDN — Add before </body>
html
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>

Code

HTML
html
<div data-live-search="" class="live-search">
  <div class="live-search__search">
    <div class="live-search__search-field">
      <i class="live-search__search-icon"><!-- search SVG --></i>
      <input type="search" placeholder="Search for name or keywords..." autocomplete="off" spellcheck="false" data-live-search-input="" class="live-search__search-input">
    </div>
  </div>
  <div class="live-search__list">
    <div class="live-search__item">
      <div class="demo-card">
        <div class="demo-card__top">
          <div class="demo-card__visual">
            <div class="demo-card__visual-before"></div>
            <span class="demo-card__emoji">🍷</span>
          </div>
          <div class="demo-card__tags-collection">
            <div class="demo-card__tags-list">
              <div class="demo-card__tags-item"><p class="demo-card__tags-item-p">Drink</p></div>
              <div class="demo-card__tags-item"><p class="demo-card__tags-item-p">Glass</p></div>
            </div>
          </div>
        </div>
        <div class="demo-card__bottom">
          <h3 class="demo-card__h3">Red Wine</h3>
        </div>
      </div>
      <!-- Hidden data used for search indexing -->
      <div class="live-search__data">
        <span class="live-search__name">Red Wine</span>
        <span class="live-search__keywords">Drink, Glass</span>
      </div>
    </div>
    <!-- Repeat .live-search__item for each card -->
  </div>
  <div data-live-search-not-found="" class="live-search__not-found">
    <p class="live-search__not-found-p">😕 We couldn't find a match for "Osmo"</p>
  </div>
</div>
CSS
css
.live-search {
  grid-column-gap: 3em;
  grid-row-gap: 3em;
  flex-flow: column;
  display: flex;
}

.live-search__search {
  justify-content: center;
  align-items: center;
  display: flex;
}

.live-search__search-field {
  background-color: #f4f4f4;
  border: 1px solid #0000;
  border-radius: 50em;
  align-items: center;
  width: 100%;
  max-width: 22em;
  height: 4em;
  display: flex;
  position: relative;
}

.live-search__search-icon {
  z-index: 1;
  pointer-events: none;
  color: #6840ff;
  -webkit-user-select: none;
  user-select: none;
  flex: none;
  justify-content: center;
  align-items: center;
  width: 1.5em;
  height: 1.5em;
  padding: 0;
  display: flex;
  position: absolute;
  left: 1em;
}

.live-search__search-input {
  letter-spacing: -.015em;
  -webkit-appearance: none;
  appearance: none;
  background-color: #0000;
  border: 0;
  outline: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0 0 0 2.75em;
  font-size: 1.125em;
  font-weight: 400;
}

.live-search__search-field:has(input:focus) {
  border-color: rgba(0, 0, 0, 0.1);
}

.live-search__search-field input::placeholder {
  color: rgba(0, 0, 0, 0.4);
  opacity: 1;
}

.live-search__list {
  grid-column-gap: 1.5em;
  grid-row-gap: 1.5em;
  flex-flow: wrap;
  justify-content: center;
  width: 100%;
  display: flex;
}

.live-search__item {
  width: calc(33.33% - 1em);
}

.live-search__not-found-p {
  color: #817f7f;
  text-align: center;
  font-size: 1em;
}

.live-search__data,
.live-search__not-found {
  display: none;
}

@media screen and (max-width: 991px) {
  .live-search__item {
    width: calc(49.995% - .75em);
  }
}

@media screen and (max-width: 767px) {
  .live-search__item {
    width: 100%;
  }
}

/* Demo card styles */
.demo-card {
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  background-color: #f4f4f4;
  border-radius: 1.5em;
  flex-flow: column;
  width: 100%;
  padding: 1em;
  display: flex;
}

.demo-card__top {
  position: relative;
}

.demo-card__visual {
  background-color: #eaeaea;
  border-radius: .5em;
  justify-content: center;
  align-items: center;
  width: 100%;
  display: flex;
  position: relative;
}

.demo-card__visual-before {
  padding-top: 66%;
}

.demo-card__emoji {
  font-size: 4em;
}

.demo-card__tags-collection {
  width: 100%;
  padding: 1em;
  position: absolute;
  top: 0;
  left: 0;
}

.demo-card__tags-list {
  display: flex;
}

.demo-card__tags-item {
  background-color: #f4f4f4;
  border-radius: 3em;
  padding: .25em .75em;
}

.demo-card__tags-item-p {
  letter-spacing: -.01em;
  margin-bottom: 0;
  font-size: .875em;
  font-weight: 400;
}

.demo-card__bottom {
  justify-content: flex-start;
  align-items: center;
  padding-bottom: .25em;
  padding-left: .5em;
  padding-right: .5em;
  display: flex;
}

.demo-card__h3 {
  letter-spacing: -.02em;
  margin-top: 0;
  margin-bottom: 0;
  font-size: 1.25em;
  font-weight: 500;
  line-height: 1.2;
}
JavaScript
javascript
function initLiveSearch() {
  document.querySelectorAll('[data-live-search]').forEach(function(root) {
    const input = root.querySelector('[data-live-search-input]');
    const notFound = root.querySelector('[data-live-search-not-found]');

    // Options — Full Documentation: https://listjs.com/
    const options = {
      listClass: 'live-search__list',
      valueNames: ['live-search__name', 'live-search__keywords'],
      fuzzySearch: {
        location: 0,
        distance: 100,
        threshold: 0.3
      }
    };

    const list = new List(root, options);

    function updateNotFound() {
      if (!notFound) return;
      const q = (input && input.value ? input.value : '').trim();
      if (list.matchingItems.length === 0 && q !== '') {
        notFound.style.display = 'block';
        const p = notFound.querySelector('p');
        if (p) p.textContent = `We couldn't find a match for "${q}" 😕`;
      } else {
        notFound.style.display = 'none';
      }
    }

    function runSearch() {
      const q = (input && input.value ? input.value : '').trim();
      if (!q) {
        list.search(); // Clear search
        updateNotFound();
        return;
      }
      if (typeof list.fuzzySearch === 'function') {
        list.fuzzySearch(q);
      } else {
        list.search(q, ['live-search__name', 'live-search__keywords']);
      }
      updateNotFound();
    }

    if (input) {
      input.addEventListener('input', runSearch);
    }

    root._pageSearchList = list;

    // Initial state
    list.search();
    updateNotFound();
  });
}

// Initialize Live Search (List.js)
document.addEventListener('DOMContentLoaded', () => {
  initLiveSearch();
});

Attributes

NameTypeDefaultDescription
data-live-searchattributeAdd to the wrapper element. Scopes the List.js instance and all child selectors to that specific search block.
data-live-search-inputattributeAdd to the <input type="search"> inside the container. The script listens for input events on this element.
data-live-search-not-foundattributeAdd to the element that shows a "no results" message. Toggled visible when the query returns zero matching items.
.live-search__listclassThe element wrapping all searchable items. List.js uses this class to identify which children to index.
.live-search__nameclassSpan inside .live-search__data holding the item's primary name. Used as a search field by List.js.
.live-search__keywordsclassSpan inside .live-search__data holding comma-separated keywords. Used as a secondary search field.
.live-search__dataclassHidden container inside each item holding .live-search__name and .live-search__keywords. Hidden via CSS — only used for indexing.

Notes

  • Requires List.js loaded via CDN before the script runs.
  • Each item needs a hidden .live-search__data block with .live-search__name and .live-search__keywords for indexing — these are not displayed to users.
  • Fuzzy search options (location, distance, threshold) can be tuned in the options object inside the script.
  • You can add additional search fields beyond name and keywords — just add matching class names to valueNames in the config.
  • The "not found" message text is updated dynamically to reflect the current query.
  • Multiple independent search blocks on the same page are supported — each [data-live-search] gets its own List.js instance.
  • Full List.js documentation: https://listjs.com/