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
| Name | Type | Default | Description |
|---|---|---|---|
| data-live-search | attribute | — | Add to the wrapper element. Scopes the List.js instance and all child selectors to that specific search block. |
| data-live-search-input | attribute | — | Add to the <input type="search"> inside the container. The script listens for input events on this element. |
| data-live-search-not-found | attribute | — | Add to the element that shows a "no results" message. Toggled visible when the query returns zero matching items. |
| .live-search__list | class | — | The element wrapping all searchable items. List.js uses this class to identify which children to index. |
| .live-search__name | class | — | Span inside .live-search__data holding the item's primary name. Used as a search field by List.js. |
| .live-search__keywords | class | — | Span inside .live-search__data holding comma-separated keywords. Used as a secondary search field. |
| .live-search__data | class | — | Hidden 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/