How to create a simple, intuitive search in JS + HTML
As a second installment to the "simple, intuitive" solution, I am to walk you through a HTML search that I've used with live websites. It's not the most robust, but its simplicty is worth sharing, and can be useful to implement in simple use cases.
For example, I use it on a page that has 40x cards, and its a quick way to navigate to a card. I don't need fuzzy searching or to query 100,000 rows.
However, searching is an important UI, and this tutorial covers more than just basic string matching on an input field after clicking a button.
Complete code is at the bottom of this page, but I will walk you step by step!
Video Tutorial
0. Boilerplate
Every project starts with boilerplate HTML. Here I have a set of cards, and a table that we can use as search data.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<style>
#card-container {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
}
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
width: 200px;
text-align: center;
}
table {
margin-top: 20px;
border-collapse: collapse;
width: 100%;
max-width: 600px;
text-align: left;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
}
th {
background-color: #f4f4f4;
}
.table-container{
display: flex;
justify-content: center;
}
</style>
<input id="cardSearchInput" type="text" placeholder="Search...">
<div id="card-container">
<div class="card">
<h3>Customer 1</h3>
<p>Name: Jane Doe</p>
<p>Email: jane.doe@example.com</p>
<p>Phone: (123) 456-7890</p>
</div>
<div class="card">
<h3>Customer 2</h3>
<p>Name: John Smith</p>
<p>Email: john.smith@example.com</p>
<p>Phone: (987) 654-3210</p>
</div>
<div class="card">
<h3>Customer 3</h3>
<p>Name: Emily Davis</p>
<p>Email: emily.davis@example.com</p>
<p>Phone: (555) 123-4567</p>
</div>
<div class="card">
<h3>Customer 4</h3>
<p>Name: Michael Brown</p>
<p>Email: michael.brown@example.com</p>
<p>Phone: (444) 987-6543</p>
</div>
</div>
<input id="tableSearchInput" type="text" placeholder="Search...">
<div class="table-container">
<table>
<thead>
<tr>
<th>Product Name</th>
<th>Category</th>
<th>Price</th>
</tr>
</thead>
<tbody id="table-container">
<tr>
<td>Wireless Headphones</td>
<td>Electronics</td>
<td>$99.99</td>
</tr>
<tr>
<td>Office Chair</td>
<td>Furniture</td>
<td>$149.99</td>
</tr>
<tr>
<td>Smartphone</td>
<td>Electronics</td>
<td>$699.99</td>
</tr>
<tr>
<td>Coffee Maker</td>
<td>Appliances</td>
<td>$49.99</td>
</tr>
<tr>
<td>Running Shoes</td>
<td>Sportswear</td>
<td>$89.99</td>
</tr>
</tbody>
</table>
</div>
</body>
Step 1. Bind our elements
We need to a) know where the search text is coming from and b) know where to search. In some contexts, this is called a needle & a haystack, shown here:
Understading our haystack is two-fold. First, and more simply, it's where we are searching through. However, secondly, and maybe more importantly, it's determining the smallest unit of what we return when we get a positive result.
As shown above, do we show the user the whole card that has a positive text match, or just the paragraph of which it exists in. It's up to you! (The developer, or what the client wants :)). However, it's common that you would return the card, snippet, row, etc. that the data lives in to give context to the data. But if it's the paragraph the user would expect to see, you would give them that.
We will define this as:
let searchInput = document.getElementById('cardSearchInput');
let cards = document.querySelectorAll('.card');
We are giving our search in "searchInput", but we are searching through the texts of .cards, so each .card is a unit; and we will also return the whole .card if we find a positive match within it.
Then we will bind a keyup function, so that our search is live to what the user is pressing. This is doable in "smaller" datasets, and will make it feel responsive.
searchInput.addEventListener('keyup', function(event){
let searchText = event.target.value.toLowerCase();
//search code
}
We now need to compare the search text against what is in each card. We visit each card individually, and ask, "does this card contain the text we are interested in?"
cards.forEach(function(cardElement){
let cardText = cardElement.innerText.toLowerCase();
if(cardText.includes(searchText)){
cardElement.style.display = 'block';
} else {
cardElement.style.display = 'none';
}
});
If the answer is yes, give the WHOLE card a style of block, if not, hide it.
That's it.
let searchInput = document.getElementById('cardSearchInput');
let cards = document.querySelectorAll('.card');
//let cards = Array.from(document.getElementById('card-container').children); // returns a live HTMLCollection, so we convert it to an array
searchInput.addEventListener('keyup', function(event){
cards.forEach(function(cardElement){
let cardText = cardElement.innerText.toLowerCase();
if(cardText.includes(searchText)){
cardElement.style.display = 'block';
} else {
cardElement.style.display = 'none';
}
});
});
If you want to abstract it, so you can bind it to any search input; search set combination, I have created a very similar function here
function bindSearch(inputId, parentId, isTable = false){
let searchInput = document.getElementById(inputId);
let searchItems = Array.from(document.getElementById(parentId).children); // returns a live HTMLCollection, so we convert it to an array
let showStyle = 'block';
if(isTable){
showStyle = 'table-row'; // when block is applied to a tr, the columns will collapse
}
searchInput.addEventListener('keyup', function(event){
let searchText = event.target.value.toLowerCase();
searchItems.forEach(function(searchElement){
let cardText = searchElement.innerText.toLowerCase();
if(cardText.includes(searchText)){
searchElement.style.display = showStyle;
} else {
searchElement.style.display = 'none';
}
});
});
}
Notice the changes
- I take in an inputId, and parentId and isTable
- Instead of having to give a class to each search element, I target the parent, and and notice that those ".cards" that we had earlier, are all just first level children of the parent
- We have to change the showStyle, if it's a table. Display block will collapse the table columns into a flex behavior
bindSearch('tableSearchInput', 'table-container', true);
After defining the function, call it to bind everything!