A search bar is one of the most common features on any website. So common in fact, that you’d be hard-pressed to find a website that deals with data in one form or another and doesn’t have a search bar. Even the most visited webpage in the world consists almost entirely of a search bar:
With that being said, in this tutorial we’ll learn how to implement a basic website search bar with Javascript and search function to filter a list of articles.
This demo reuses the layout from two previous tutorials. These tutorials also discuss how the data is fetched and displayed and how the styling and layout is setup.
For our updated layout, we’ll be adding an HTML search bar to the header element and an element to hold the text for the search results so this is what the HTML markup looks like:
1 |
|
2 |
|
3 |
id="search" type="search" placeholder="🔍 Start typing to search..." />
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
We use the type="search"
to ensure the semantic nature of our input field. It also provides an inbuilt clear search button.
Another feature we can include in our HTML markup is a list of search suggestions that appear in a dropdown when the user clicks on the search input field. This can be achieved using the element and
list
attribute:
This website search demo uses the same styling from the previously listed tutorials with a few additions: we’ve updated the header to be sticky so the search bar is always at top while scrolling, and hidden the default styling for the input list field.
1 |
header { |
2 |
position: sticky; |
3 |
top: -54px; /* value to scroll past the logo so just the search bar is sticky */ |
4 |
z-index: 2; |
5 |
}
|
6 |
|
7 |
.search-bar { |
8 |
display: flex; |
9 |
justify-content: center; |
10 |
padding: 24px; |
11 |
}
|
12 |
|
13 |
.search-bar input { |
14 |
width: 50%; |
15 |
min-width: 300px; |
16 |
padding: 12px 24px; |
17 |
border-radius: 24px; |
18 |
font-size: 16px; |
19 |
border: 0px; |
20 |
outline: none; |
21 |
}
|
22 |
|
23 |
.search-bar [list]::-webkit-list-button, |
24 |
.search-bar [list]::-webkit-calendar-picker-indicator { |
25 |
display: none !important; |
26 |
}
|
We’ll be using the Fetch API to retrieve mock data scraped from the Tuts+ authors page and stored in a Github gist and then appending it to the page. The logic behind this is explained in this tutorial How to Filter Data on a Webpage (with JavaScript) and this is what the code looks like:
1 |
let postsData = ""https://webdesign.tutsplus.com/; |
2 |
const postsContainer = document.querySelector("https://webdesign.tutsplus.com/.posts-container"https://webdesign.tutsplus.com/); |
3 |
|
4 |
fetch( |
5 |
"https://webdesign.tutsplus.com/https://gist.githubusercontent.com/jemimaabu/564beec0a30dbd7d63a90a153d2bc80b/raw/0b7e25ba0ebee6dbba216cfcfbae72d460a60f26/tutorial-levels" |
6 |
).then(async (response) => { |
7 |
postsData = await response.json(); |
8 |
postsData.map((post) => createPost(post)); |
9 |
});
|
10 |
|
11 |
const createPost = (postData) => { |
12 |
const { title, link, image, categories } = postData; |
13 |
const post = document.createElement("https://webdesign.tutsplus.com/div"https://webdesign.tutsplus.com/); |
14 |
post.className = "https://webdesign.tutsplus.com/post"https://webdesign.tutsplus.com/; |
15 |
post.innerHTML = ` |
16 |
${link}" target="_blank"> |
17 |
${image}"> |
18 |
|
19 |
|
20 |
${title} |
21 |
|
22 |
${categories |
23 |
.map((category) => { |
24 |
return "https://webdesign.tutsplus.com/' + category + "https://webdesign.tutsplus.com/"https://webdesign.tutsplus.com/; |
25 |
})
|
26 |
.join(""https://webdesign.tutsplus.com/)} |
27 |
|
28 |
|
29 |
`; |
30 |
|
31 |
postsContainer.append(post); |
32 |
};
|
Now we can get to the search logic.
First, we’ll add an input listener to the search field to detect when text is being typed. When working with event listeners, it’s considered best practice to control how many times the event is called. We can do that using our debounce function.
1 |
const search = document.getElementById("https://webdesign.tutsplus.com/search"https://webdesign.tutsplus.com/); |
2 |
|
3 |
let debounceTimer; |
4 |
const debounce = (callback, time) => { |
5 |
window.clearTimeout(debounceTimer); |
6 |
debounceTimer = window.setTimeout(callback, time); |
7 |
};
|
8 |
|
9 |
search.addEventListener( |
10 |
"https://webdesign.tutsplus.com/input"https://webdesign.tutsplus.com/, |
11 |
(event) => { |
12 |
const query = event.target.value; |
13 |
debounce(() => handleSearchPosts(query), 500); |
14 |
},
|
15 |
false
|
16 |
);
|
In this function, we call the handleSearchPosts
function inside our debounce function with a debounce time of 500ms. Next we’ll define the logic inside the handleSearchPosts()
function.
When working with user input, it’s a good idea to format the input in some way to prevent unpredictable results. In the case of our search input, we’ll be removing any extra whitespace and also only returning search results for input with more than one character. We’ll also convert the function to lowercase to prevent any case sensitive results. This is what the function looks like so far:
1 |
const handleSearchPosts = (query) => { |
2 |
const searchQuery = query.trim().toLowerCase(); |
3 |
|
4 |
if (searchQuery.length <= 1) { |
5 |
return
|
6 |
}
|
7 |
}
|
Next, we want to handle fetching the right data based on the query. We’ll be returning any post where the title or category text contains the query:
1 |
let searchResults = [...postsData].filter( |
2 |
(post) => |
3 |
post.categories.some((category) => category.toLowerCase().includes(searchQuery)) || |
4 |
post.title.toLowerCase().includes(query) |
5 |
);
|
The post categories is an array so we use the some()
method to return any matches and the .includes()
method to check if the string contains the query . The includes method is case sensitive so we convert the post category and title to lowercase before searching.
Now we’ve gotten our search results, we can update the search display text to display text based on the results returned:
1 |
const searchDisplay = document.querySelector("https://webdesign.tutsplus.com/.search-display"https://webdesign.tutsplus.com/); |
2 |
|
3 |
if (searchResults.length == 0) { |
4 |
searchDisplay.innerHTML = "https://webdesign.tutsplus.com/No results found" |
5 |
} else if (searchResults.length == 1) { |
6 |
searchDisplay.innerHTML = `1 result found for your query: ${query}` |
7 |
} else { |
8 |
searchDisplay.innerHTML = `${searchResults.length} results found for your query: ${query}` |
9 |
}
|
Then we append our search results on the page using the createPost()
function.
1 |
postsContainer.innerHTML = ""https://webdesign.tutsplus.com/; |
2 |
searchResults.map((post) => createPost(post)); |
We want to be able to reset the page to its default state when the user clears the input field. We can do this by defining a resetPosts()
function and calling it in our handleSearchPosts()
function
1 |
const resetPosts = () => { |
2 |
searchDisplay.innerHTML = "" |
3 |
postsContainer.innerHTML = ""https://webdesign.tutsplus.com/; |
4 |
postsData.map((post) => createPost(post)); |
5 |
};
|
6 |
|
7 |
|
8 |
const handleSearchPosts = (query) => { |
9 |
const searchQuery = query.trim().toLowerCase(); |
10 |
|
11 |
if (searchQuery.length <= 1) { |
12 |
resetPosts() |
13 |
return
|
14 |
}
|
15 |
|
16 |
let searchResults = [...postsData].filter( |
17 |
(post) => |
18 |
post.categories.some((category) => category.toLowerCase().includes(searchQuery)) || |
19 |
post.title.toLowerCase().includes(searchQuery) |
20 |
);
|
21 |
|
22 |
if (searchResults.length == 0) { |
23 |
searchDisplay.innerHTML = "https://webdesign.tutsplus.com/No results found" |
24 |
} else if (searchResults.length == 1) { |
25 |
searchDisplay.innerHTML = `1 result found for your query: ${query}` |
26 |
} else { |
27 |
searchDisplay.innerHTML = `${searchResults.length} results found for your query: ${query}` |
28 |
}
|
29 |
|
30 |
postsContainer.innerHTML = ""https://webdesign.tutsplus.com/; |
31 |
searchResults.map((post) => createPost(post)); |
32 |
};
|
With this, we have our simple search bar completely set up!