If you have a page that includes a lot of information, it’s a good idea to let users search for what they might be looking for. I’m not talking about searching a database or even searching JSON data — I’m talking about literally searching text on a single rendered web page. Users can already use the built-in browser search for this, but we can augment that by offering our own search functionality that filters down the page making matching results easier to find and read.
Here’s a live demo of what we’re going to build:
I use this same technique on my real project: https://freestuff.dev/.
Meet JavaScript!
Well, you might know JavaScript already. JavaScript is going to handle all the interactivity in this journey. It’s going to…
- find all the content we want to search through,
- watch what a user types in the search input,
- filter the
innerText
of the searchable elements, - test if the text includes the search term (
.includes()
is the heavy lifter here!), and - toggle the visibility of the (parent) elements, depending on if they include the search term or not.
Alright, we have our requirements! Let’s start working.
The basic markup
Let’s assume we have a FAQ page. Each question is a “card” which has a title and content:
<h1>FAQ Section</h1>
<div class="cards">
<h3>Who are we</h3>
<p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized </p>
</div>
<div class="cards">
<h3>What we do</h3>
<p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized </p>
</div>
<div class="cards">
<h3>Why work here</h3>
<p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized</p>
</div>
<div class="cards">
<h3>Learn more</h3>
<p>Want to learn more about us?</p>
</div>
Imagine there are a lot of questions on this page.
To get ready for the interactivity, we’ll have this one line of CSS. This gives us a class we can add/remove depending on the search situation when we get to the JavaScript:
.is-hidden { display: none; }
Let’s add a search input with an event that fires when it is interacted with:
<label for="searchbox">Search</label>
<input
type="search"
oninput="liveSearch()"
id="searchbox"
>
The JavaScript baseline
And here’s the JavaScript that does everything else!
function liveSearch() {
// Locate the card elements
let cards = document.querySelectorAll('.cards')
// Locate the search input
let search_query = document.getElementById("searchbox").value;
// Loop through the cards
for (var i = 0; i < cards.length; i++) {
// If the text is within the card...
if(cards[i].innerText.toLowerCase()
// ...and the text matches the search query...
.includes(search_query.toLowerCase())) {
// ...remove the `.is-hidden` class.
cards[i].classList.remove("is-hidden");
} else {
// Otherwise, add the class.
cards[i].classList.add("is-hidden");
}
}
}
You can probably go line-by-line there and reason out what it is doing. It finds all the cards and the input and saves references to them. When a search event fires, it loops through all the cards, determines if the text is within the card or not. It the text in the card matches the search query, the .is-hidden
class is removed to show the card; if not, the class is there and the card remains hidden.
Here is the link to the demo again.
Adding a delay
To make sure our JavaScript doesn’t run too much (meaning it would slow down the page), we will run our liveSearch
function only after waiting an “X” number of seconds.
<!-- Delete on Input event on this input -->
<label for="searchbox">Search</label>
<input type="search" id="searchbox">
// A little delay
let typingTimer;
let typeInterval = 500; // Half a second
let searchInput = document.getElementById('searchbox');
searchInput.addEventListener('keyup', () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(liveSearch, typeInterval);
});
What about fuzzy searches?
Let’s say you want to search by text that is not visible to user. The idea is sort of like a fuzzy search, where related keywords return the same result as an exact match. This helps expand the number of cards that might “match” a search query.
There are two ways to do this. The first is using a hidden element, like a span, that contains keywords:
<div class="cards">
<h3>Who are we</h3>
<p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized</p>
<!-- Put any keywords here -->
<span class="is-hidden">secret</span>
</div>
Then we need to update our liveSearch function. Instead of using .inner
Text we will use .textContent
to includes hidden elements. See more detail about the difference between innerText and textContent here
for (var i = 0; i < cards.length; i++) {
if(cards[i].textContent.toLowerCase()
.includes(search_query.toLowerCase())) {
cards[i].classList.remove("is-hidden");
} else {
cards[i].classList.add("is-hidden");
}
}
Try typing “secret” on a search box, it should reveal this card, even though “secret” isn’t a displayed on the page.
A second approach is searching through an attribute. Let’s say we have a gallery of images. We can put the keywords directly on the alt
attribute of the image. Try typing “kitten” or “human” in the next demo. Those queries are matching what’s contained in the image alt
text.
For this to work, we need to change innerText
to getAttribute('alt')
since we want to look through alt
attributes in addition to what’s actually visible on the page.
for (var i = 0; i < cards.length; i++) {
if(cards[i].getAttribute('alt').toLowerCase()
.includes(search_query.toLowerCase())) {
cards[i].classList.remove("is-hidden");
} else {
cards[i].classList.add("is-hidden");
}
}
Depending on your needs, you could put your keywords in another attribute, or perhaps a custom one.
Caveat
Again, this isn’t a search technology that works by querying a database or other data source. It works only if you have all the searchable content in the DOM on that page, already rendered.
So, yeah, there’s that. Just something to keep in mind.
Wrapping up
Obviously, I really like this technique, enough to use it on a production site. But how else might you use something like this? An FAQ page is a clear candidate, as we saw, but any situation that calls for filtering any sort of content is fit for this sort of thing. Even a gallery of images could work, using the hidden input trick to search through the alt
tag content of the images.
Whatever the case, I hope you find this helpful. I was surprised that we can get a decently robust search solution with a few lines of vanilla JavaScript.
Have you used this technique before, or something like it? What was your use case?
I use a table filter and list filter quite often, when getting all the results is faster than paging, because filtering is faster than searching. The one your using doesn’t work for my case, because IE still exists in my world.
https://gist.github.com/miwebguy/417160798c8c1b275a0d15d04bf01954
Thank you Bill for sharing
Thanks for the cool tutorial! I use similar techniques a lot myself, but they are possibly not fully accessible, or at least don’t fully comply with accessibility standards.
Forms without a submit button (e.g. filter forms, that immediately filter when you type or use a select element) are problematic, I suggest this link as a starting point for further research: https://stackoverflow.com/questions/42811178/is-it-possible-to-make-a-accessible-form-without-a-submit-button
I myself still build filters without submit buttons, especially if the data is already loaded, and the submit button would just slow things down with an unneccessary extra click. Some users may find this annoying or harder to use, but it shouldn’t make the filter inaccessible. To make things more accessible, I include a status message using role=”status”:
You can read about the attribute here: https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA22
If anyone knows more about how (or if) filters without submit button can be made accessible, that would be highly appreciated!
Thank you Jonathan for the reference
Jonathan’s comment needs starred. Very helpful.
Might sound cheesy, but including a search button is less confusing to people if they expect to click on something. So, including a search button might be a good thing, even if the search is “live”.
Of course, there is plenty of space for optimization: for instance, keeping a list of elements instead of re-querying them every time. I was also going to say “filtering only the visible ones when, on keyup, the input value isn’t longer” but this would not work with copy/paste.
Good! could be better if you use JSON for searching and generating the content. Then you don’t need to hide keywords inside html.
The fuse library is a great fuzzy search you can use.
By accident I published similar but much smaller (825b) npm package for searching and filtering JSON arrays.
You could have used better semantic HTML with definition list (DL DT and DD elements)
Also, using data attributes (data-*) can be more relevant to add data to HTML elements that are meant to be manipulated with JavaScript
Such amazing tut.. mas hilman keren!!!
Everything ok. But…
In polish language try tu use for search ‘Śreniawa’ and nothing working ok… :(
it doesn’t work when you click on ‘x’ to remove search.
Also it is inefficient, you’re transforming search to lowercase for each filterable item.
When your list gets long, this method is going to have a performance impact.
A great solution I’ve seen is what Jets.js has done, where they manipulate a single css rule so that it hides or shows the matching search rule.
So if you’re searching for “bob”, the engine will dynamically write out something like
and let the css engine handle the visual filtering.
Vanilla JS solution for longer lists. Mine had 1700 items.
let timer;
const input = document.querySelector("#searchbox");
input.addEventListener("keyup", function (liveSearch) {
clearTimeout(timer);
timer = setTimeout(() => {
const items = document.querySelectorAll(".link_button");
for (let item of items) {
item.style.display = item.textContent.includes(liveSearch.target.value)
? "inline-block"
: "none";
}
}, 1000);
});
is there any way possible to do something like this but the function is to search through multiple pages. Only using Javascript?
Hey! How can I add a button so the results appear? TX
Fantastic tutorial, used it to provide search terms on tables 1000s of rows big! Data is static so no biggie!
Ran into an issue while running with React…
The element would render after the event listener was added, so I just added a try/catch block to resolve the issue.
Thanks again!
I don’t understand why Bulma is a requirement for this. If I remove bulma is a requirement for this? That’s a CSS lib and when I remove it, the JS no longer works, but you don’t mention that in your article.
Is there any way to animate this so it looks a little nicer transition-wise?
How i add categorys to filter? :(
I suppose this is a basic question but JS is new to me. I have been using livesearch and have 212 cards which make up a membership directory with the page framework BS 4.6 and PHP and when I search the filter it is instant but I would like to display the amount of cards filtered so take the JS total and pass to PHP variable. Could you provide a pointer to how I can achieve this. Great script though