Displaying the live search results to the user while they are typing in the search bar is a great user experience. But creating the live search field with all the features. Like, select with a mouse, move selections on key up down and hide the search bar on blur.
This is a little bit of a challenging task. So, let's try to go through all the challenges and create this awesome live search field. Where users can select the result with a mouse or by using the keyboard also the results will be hidden if we click outside.
The things we will cover
- React & Tailwind Project Setup
- Creating a Search Bar
- Container to Render Search Results
- Creating a Fully Customizable Component
- Conditionally Rendering Results
- Adding a Live Search Logic
- Selecting Results on Key up-down
- Styling the Focused Item
- Selecting the Results
- Hiding Results onBlur
- Updating the value inside the search bar
React & Tailwind Project Setup
To create this demo we are going to use obviously the React JS and Tailwind CSS for styling. If you want to you can use any other CSS framework or library. This logic will work for all of them.
If you don't know Tailwind CSS is a framework with a bunch of utility class names to style our HTML.
If you already have a project, you can directly add this logic to your existing project as well. But if you are starting from the ground like me run these commands to initialize a new project.
npx create-react-app live-search --template typescript
# or
npx create-react-app live-search // if you are using JavaScript
Once you run one of the commands, the project will be initialized. Now you can install Tailwind CSS. First, change the directory with cd live-search
and run these commands.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Then you have to go to your tailwind.config.js
file and add these things inside your content.
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}",],
...
Then go to your index.css
and replace all of the code with these three lines of code.
// index.css
// remove old CSS and add them
@tailwind base;
@tailwind components;
@tailwind utilities;
Creating a Search Bar
A search bar is just an input
element. But we will add a little bit of style to it so that it looks a little bit nice. For that, let's create a file called SearchBar.tsx
(or jsx) inside the src folder and add these codes.
// inside SearchBar.tsx
import React, { FC } from "react";
interface Props<T> {
value?: string;
notFound?: boolean;
onChange?: React.ChangeEventHandler;
results?: T[];
renderItem: (item: T) => JSX.Element;
onSelect?: (item: T) => void;
}
const SearchBar = <T extends object>({ }: Props<T>): JSX.Element => {
return (
{/* Search bar */}
<input
type="text"
className="w-full p-2 text-lg rounded border-2 border-gray-500 outline-none"
placeholder="Search here..."
/>
);
};
I don't think I have to explain these class names. They are explaining themselves. But these interface Props
might look a little scary. This is just a Typescript thing which I will explain when we need them. So, let's render this component inside our App.tsx
// inside App.tsx
const App = () => {
return (
<div className="max-w-3xl mx-auto mt-10">
<SearchBar />
</div>
);
};
export default App;
Here I am using the fixed width for the div
so that I can use this mx-auto
to bring this div
to the center of the webpage. If you try to run this code it will throw an error. Because renderItem
is the required prop. Comment it out if you want to see the change.
Container to Render Search Results
Now we need a container to render our results. So for that, we will wrap our search input inside a div
with class relative
. And we will render a div
below the input element with class absolute. With these classes, we will render the search container right below the input field.
// SearchBar.tsx
const SearchBar = <T extends object>({ }: Props<T>): JSX.Element => {
return (
<div className="relative">
<input ... />
{/* Results Wrapper */}
<div className="absolute mt-1 w-full p-2 bg-white shadow-lg rounded-bl rounded-br max-h-36 overflow-y-auto"></div>
</div>
);
};
With the help of other classes, like max-h-36 overflow-y-auto
. It will create a container with a fixed height and show the scroll bar if the results are longer.
Creating a Fully Customizable Component
Now let's use the first props from inside the interface. As you see here we are using a little weird syntax for the interface
and SearchBar
components.
interface Props<T> {
results?: T[];
renderItem: (item: T) => JSX.Element;
onSelect?: (item: T) => void;
}
const SearchBar = <T extends object>({}:Props<T>) ...
This is because we want to make this search bar so customizable. So that, it can take, render and give the same data type back to our renderItem
and onSelect
methods. Now you don't need to worry about types anymore. Typescript will grab the type from results
and distribute it to renderItem
and onSelect
.
Now finally see how we can render our results.
// inside SearchBar.tsx
const SearchBar = <T extends object>({ results = [], renderItem }: Props<T>): JSX.Element => {
return (
<div className="relative">
{/* Search bar */}
<input ... />
{/* Results Wrapper */}
<div className="...">
{/* Results Lists Goes here */}
<div className="space-y-2">
{results.map((item, index) => {
return (
<div
key={index}
className="cursor-pointer hover:bg-black hover:bg-opacity-10 p-2"
>
{renderItem(item)}
</div>
);
})}
</div>
</div>
</div>
);
};
Very first thing, don't forget to give your results
prop a default value empty array []
. It will prevent unwanted errors. Then we are using resutls.map
to render our results. Where we have renderItem
method to render results. We have to pass this method from App.tsx
which we will see later.
Then we have a wrapper div for renderItem
with classes, hover:bg-black hover:bg-opacity-10
. It is very important to give the user feedback on results hover. These classes will add background-color
of black with opacity: 0.1
on the result hover.
Ok, now let's try to pass some data to our SearchBar
. Because we used that <T>
generic type we can pass any type of data but inside an array.
const profiles = [
{ id: "1", name: "Allie Grater" },
{ id: "2", name: "Aida Bugg" },
{ id: "3", name: "Gabrielle" },
{ id: "4", name: "Grace" },
{ id: "5", name: "Hannah" },
{ id: "6", name: "Heather" },
{ id: "7", name: "John Doe" },
{ id: "8", name: "Anne Teak" },
{ id: "9", name: "Audie Yose" },
{ id: "10", name: "Addie Minstra" },
{ id: "11", name: "Anne Ortha" },
];
const App = () => {
return (
<div className="max-w-3xl mx-auto mt-10">
<SearchBar results={profiles} renderItem={(item) => <p>{item.name}</p>} />
</div>
);
};
As I told you earlier you don't need to worry about the types for renderItem
and onSelect
. Here is the result if you hover over an item you will see its type. Cool right?
Now if you save your file and run the project with npm start
. You will see something like this.
Conditionally Rendering Results
Currently, these results are always visible so, let's solve this issue. For that, we can simply add a state called showResults
and display the results according to the value of this state.
// inside SearchBar.tsx
const SearchBar = ... {
const [showResults, setShowResults] = useState(true);
// To toggle the search results as result changes
useEffect(() => {
if (results.length > 0 && !showResults) setShowResults(true);
if (results.length <= 0) setShowResults(false);
}, [results]);
return ...
}
The logic is pretty straightforward if there are results and the showResults
state is false
only then make it true
. Otherwise, make it false. These logics will now hide and show our search results as the result changes its length.
But for that, we have to add the condition if showResults
is true
only then render the results.
const SearchBar = ... {
...
return (
<div className="relative">
{/* Search bar */}
<input ... />
{/* Results Wrapper */}
{showResults ? <div className="...">
{/* Results Lists Goes here */}
<div className="space-y-2">...</div>
</div> : null}
</div>
);
};
Adding a Live Search Logic
Now let's write a little piece of code to add a fake search logic.
// SearchBar.tsx
const SearchBar = <T extends object>({onChange, ... }: Props<T>): JSX.Element => {
return (...
<input
onChange={onChange}
/>
...
// App.tsx
const profiles = [...]
const App = () => {
const [results, setResults] = useState<{ id: string; name: string }[]>();
type changeHandler = React.ChangeEventHandler<HTMLInputElement>;
const handleChange: changeHandler = (e) => {
const { target } = e;
if (!target.value.trim()) return setResults([]);
const filteredValue = profiles.filter((profile) =>
profile.name.toLowerCase().startsWith(target.value)
);
setResults(filteredValue);
};
return (
<div className="max-w-3xl mx-auto mt-10">
<SearchBar
onChange={handleChange}
results={results}
renderItem={(item) => <p>{item.name}</p>}
/>
</div>
);
};
Even though this is too much code the logic is simple. First, we destructure the onChange
inside SearchBar
component and assigned it to the input element.
Then inside App.tsx
we are handling the change event for SearchBar
. So for the handleChange
we are just using the array filter
method to filter out the matching results.
If we found the results we will add them to the new state called results and if we have an empty input field we will reset those results to an empty array.
And at the end instead of the profiles
, we will pass the results
state to the results
prop. If you do this you should see the search results only if you type out the correct profile names.
Inside the handleChange
you can also fetch the data from the server. The only important thing is how will you add or reset your results
state. Because if you forgot to reset the results
state the results inside your live search container will be visible all the time.
Selecting Results on Key up-down
It's nice we have our search bar where we can search and render the results. But now we need to add a keyboard event to move our selection up and down.
const SearchBar = ... => {
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
const { key } = e;
// move down
if (key === "ArrowDown"){ }
// move up
if (key === "ArrowUp"){ }
// hide search results
if (key === "Escape"){ }
// select the current item
if (key === "Enter"){ }
};
return (
<div
tabIndex={1}
onKeyDown={handleKeyDown}
className="relative outline-none"
>
<input ... />
...
We want to now listen to the keyDown
event and we are doing that to the wrapping div
inside SearchBar
component. But we can not directly listen to the keyDown
for divs. So we need to use the tabIndex
prop. Also to avoid unwanted outlines on div focus (because we are using tabIndex
there will be an outline) I am using the outline-none
class.
Let's write down the keyboard logic one by one.
Handling the keyboard event is not that straightforward. Because while we are moving up or down we have to handle multiple things. Like displaying the correct UI, and scrolling in the right position if there are multiple results.
So for that first, we need a state called focusedIndex
. As you can see the default value for this state is -1. Because later we will use this state to move our focus to the correct place. And we don't want to focus on anything at first. So -1 will work fine.
// inside SearchBar.tsx
const SearchBar = ... => {
const [focusedIndex, setFocusedIndex] = useState(-1);
...
Now with the logic below it will update our focused index with the correct timing. Also if you notice I am not using -1 or +1 to update our value. Instead, we are using the modulo
(%) operater.
This is a small formula that we can use to update our count only within the range according to the length of the results. So if you have results with the length 6 and if you press the down key then the nextCount
will start from 0 and goes to 5. If you press up this will be the opposite starts from 5 and goes to 0.
Which is the exact value that we need to be inside the bound of the result array. If we go beyond the length we will get index bound exception.
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
let nextCount = 0;
if (key === "ArrowDown") nextCount = (focusedIndex + 1) % results.length;
if (key === "ArrowUp") nextCount = (focusedIndex + results.length - 1) % results.length;
if (key === "Escape") setShowResults(false);
if (key === "Enter") {
e.preventDefault();
}
setFocusedIndex(nextCount);
};
Also, there is one more thing happening. If we press the Escape
key we will hide the search results. But we are not handling the Enter
we will do that later.
Styling the Focused Item
Now according to the focusedIndex
we need to style the result. For that, we have to take a reference of specific results and change the style.
// SearchBar.tsx
const SearchBar = ... => {
const resultContainer = useRef<HTMLDivElement>(null);
return (
...
{results.map((item, index) => {
return (
<div
ref={index === focusedIndex ? resultContainer : null}
style={{ backgroundColor: index === focusedIndex ? "rgba(0,0,0,0.1)" : "" }}
key={index}
className="cursor-pointer hover:bg-black hover:bg-opacity-10 p-2"
>
{renderItem(item)}
</div>
);
})}
The main thing for this div
is the ref
and the style
prop. What basically we are doing? If the index
is equal to the focusedIndex
we are assigning this div
to the ref
called resultContainer
otherwise we are making it null. Why? You will understand it later.
Then we are changing the background color to black with an opacity of 10% if the index
is equal to the focusedIndex
.
Now with this logic, if you try to move up or down you will see the change but the results will stay in one place only. It will not scroll down or up as you update your index. To fix this use this logic.
// SearchBar.tsx
useEffect(() => {
if (!resultContainer.current) return;
resultContainer.current.scrollIntoView({
block: "center",
});
}, [focusedIndex]);
If there is resultContainer
we will use scrollIntoView
method to scroll in to the exact point and we will make the focused item in the center with the option block: center
.
Now you should have something like this.
Selecting the Results
One of the important things for live search is selecting the result so let's see how we can do that.
// SearchBar.tsx
...
const handleSelection = (selectedIndex: number) => {
const selectedItem = results[selectedIndex];
if (!selectedItem) resetSearchComplete();
onSelect && onSelect(selectedItem);
resetSearchComplete();
};
const resetSearchComplete = useCallback(() => {
setFocusedIndex(-1);
setShowResults(false);
}, []);
...
We need to now handle two things for selection first select the correct thing and send it back to the App.tsx
. Because this is the component where we are using it right now. Then we also need to reset the search results.
For that, inside handleSelection
we are accepting selectedIndex
and extracting the selected item from inside the results array. If there is nothing just return otherwise call the onSelect
with the selected result. Make sure you are destructuring it from the props.
const SearchBar = ({ onSelect, ...}) =>
And then simply reset the states to their previous values. focusedIndex
to -1 and showResults
to false.
Now we need to add this handleSelection
in two places. Inside Enter key press and on mouse down for the result item.
// SearchBar.tsx
// to select on enter
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
...
if (key === "Enter") {
e.preventDefault();
handleSelection(focusedIndex);
}
setFocusedIndex(nextCount);
};
return (
...
{results.map((item, index) => {
return (
<div
onMouseDown={() => handleSelection(index)}
ref={index === focusedIndex ? resultContainer : null}
style={{ backgroundColor: index === focusedIndex ? "rgba(0,0,0,0.1)" : "" }}
...
>
{renderItem(item)}
</div>
);
With this now if you try to select with a mouse or keyboard you will select the correct item. If you want to see that add onSelect
inside your SearchBar
and log the result to the console.
// App.tsx
const App = () => {
return (
<div className="max-w-3xl mx-auto mt-10">
<SearchBar
...
onSelect={(item) => console.log(item)}
/>
</div>
);
}
Hiding Results onBlur
Now if you are thinking why onMouseDown
why not onClick
? Instead of giving you the answer, I will show you that.
Just add onBlur
to your top wrapper div
inside SearchBar
component. And pass resetSearchComplete
method to it.
// SearchBar.tsx
...
return (
<div
tabIndex={1}
onKeyDown={handleKeyDown}
onBlur={resetSearchComplete}
className="relative outline-none"
>
...
Now if you click outside the search results will be gone which is the perfect result we want. Also, you will see the selected results inside your console (if you select any). But now if you change that onMouseDown
to onClick
results will be gone from the screen but you will not get the selected item inside the console.
Hmmm, why is that? This is because before onClick
the onBlur
will be fired. So, handleSelection
will be never called.
Updating the value inside the search bar
Everything looks fine here. The only thing that we need now is to display the selected profile name inside our search bar. So, create a state called defaultValue
inside SearchBar.tsx
. Then pass this value to the input
element.
// SearchBar.tsx
const SearchBar = ... ({ value }) => {
const [defaultValue, setDefaultValue] = useState("");
...
return ...
<input
...
value={defaultValue}
/>
If you remember we are also accepting a prop called value for our SearchBar
component. So, if there is value
prop available let's assign that to the defaultValue
.
// SearchBar.tsx
useEffect(() => {
if (value) setDefaultValue(value);
}, [value]);
Now if the value changes this useEffect
hook will update the defaultValue
to the value
.
Let's do one more thing inside App.tsx
. Add a state called selectedProfile
and update the value inside onSelect
. Then pass selectedProfile.name
to the value prop
.
// App.tsx
const App = () => {
const [selectedProfile, setSelectedProfile] = useState<{ id: string; name: string }>();
return (
...
<SearchBar
...
onSelect={(item) => setSelectedProfile(item)}
value={selectedProfile?.name}
/>
Now ladies and gentlemen you should see something like this.
But there is one big problem. Once you have selected the result you can not make any changes to the input field.
To solve this issue we have to handle the onChange
event directly inside our SearchBar
component.
// SearchBar.tsx
const SearchBar = ({ onChange }) => {
...
type changeHandler = React.ChangeEventHandler<HTMLInputElement>;
const handleChange: changeHandler = (e) => {
setDefaultValue(e.target.value);
onChange && onChange(e);
};
return (
...
{/* Search bar */}
<input
onChange={handleChange}
...
Here instead of passing onChange
prop directly to the input
element. Now we will pass handlChange
and inside here we will update the defaultValue
and then call the onChange
prop. With this now you can update the value with onSelect
and this SearchBar
will automatically reset the value when the onChange
occurs.
And I think this is more than enough for this project. You can see the final look and if you have anything to say at the bottom you will find my social media links.
Top comments (0)