Demo
React
Next.js
TailwindCSS
TypeScript
Node.js
JavaScript
Introduction
In this guide, we're diving into how to create a super cool, customizable search bar that you can tailor to fit your needs, just like Google’s search functionality. We’ll cover everything from making the search bar itself to adding advanced features like regex filtering and making sure it’s accessible for everyone. Plus, we’ll throw in some tips on styling and handling search events to give it that extra polish.
Tailwind CSS Utility Function
Here, we'll set up a handy utility function to help manage Tailwind CSS classes. This function will make it easier to handle conditional class names and merge Tailwind classes properly. If you're not a fan of this approach, you can always manage the class names directly in your components.
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx (inputs))
}
Create a Search Bar Component
We’ll build a basic search bar component that shows or hides based on whether it's open. Instead of toggling it in and out of existence, we’ll control its visibility using state. This lets us use smooth CSS transitions and animations to make the user experience a bit fancier.
import React, { useState } from 'react' ;
import { cn } from '@/lib/utils' ;
const SearchBar = () => {
const [ open , setOpen ] = useState ( false )
return (
< div className = "relative" >
< input
type = "text"
placeholder = "Search..."
/>
//
< div className = { cn ( "absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center hidden" ,{
"block" :open
})}>
</ div >
</ div >
)
}
Handling the Open and setOpen state.
We’ll build a basic search bar component that shows or hides based on whether it's open. Instead of toggling it in and out of existence, we’ll control its visibility using state. This lets us use smooth CSS transitions and animations to make the user experience a bit fancier.
import React, { useState ,useCallback} from 'react' ;
import { cn } from '@/lib/utils' ;
const SearchBar = () => {
const [ open , setOpen ] = useState ( false )
const handleBlur = useCallback (() => {
// Delay closing to allow click event to register
setTimeout (() => setOpen ( false ), 200 );
}, []);
return (
< div className = "relative" >
< input
type = "text"
placeholder = "Search..."
onFocus = {() => {
setOpen ( true );
}}
/>
//
< div className = { cn ( "absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center hidden" ,{
"block" :open
})}>
</ div >
</ div >
)
}
Adding Search Items.
We’ll extend our search bar component to accept an array of options that will appear as suggestions when the search bar is open. This dropdown will help users find what they’re looking for more quickly by showing relevant suggestions based on their input.
import React, { useState ,useCallback} from 'react' ;
import { cn } from '@/lib/utils' ;
export type SearchCompleteProps = {
options : string [];
placeholder ?: string ;
};
const SearchBar = ({
options,
placeholder = "Search here..." ,
} : SearchCompleteProps ) => {
const [ open , setOpen ] = useState ( false )
const handleBlur = useCallback (() => {
// Delay closing to allow click event to register
setTimeout (() => setOpen ( false ), 200 );
}, []);
return (
< div className = "relative" >
< input
type = "text"
placeholder = {placeholder}
onFocus = {() => {
setOpen ( true );
}}
/>
< div className = { cn ( "absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center hidden bg-white shadow-lg rounded-md" ,{
"block" :open
})}>
{options. map (( option , index ) => (
< div
key = {option}
>
{option}
</ div >
))}
</ div >
</ div >
)
}
To make our search bar more user-friendly and accessible, we’ll add keyboard navigation. This will let users move through the search suggestions using their keyboard and select options with the Enter key, improving the overall usability.
import React, { useState ,useCallback} from 'react' ;
import { cn } from '@/lib/utils' ;
export type SearchCompleteProps = {
options : string [];
placeholder ?: string ;
};
const SearchBar = ({
options,
placeholder = "Search here..." ,
} : SearchCompleteProps ) => {
const [ open , setOpen ] = useState ( false );
const [ inputValue , setInputValue ] = useState ( "" );
const handleBlur = useCallback (() => {
// Delay closing to allow click event to register
setTimeout (() => setOpen ( false ), 200 );
}, []);
const handleInputChange = useCallback (
( event : React . ChangeEvent < HTMLInputElement >) => {
const value = event.target.value;
setInputValue (value);
setOpen ( true );
},
[updateFilteredOptions],
);
return (
< div className = "relative" >
< input
type = "text"
placeholder = {placeholder}
onFocus = {() => {
setOpen ( true );
}}
onChange = {handleInputChange}
/>
< div className = { cn ( "absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center hidden bg-white shadow-lg rounded-md" ,{
"block" :open
})}>
{options. map (( option , index ) => (
< div
key = {option}
>
{option}
</ div >
))}
</ div >
</ div >
)
}
Adding Keyboard Navigation
To improve accessibility and user experience, we'll add keyboard navigation to the search bar. This allows users to navigate through search suggestions using the keyboard and select an option with the Enter key.
import React, { useState ,useCallback,useRef} from 'react' ;
import { cn } from '@/lib/utils' ;
export type SearchCompleteProps = {
options : string [];
placeholder ?: string ;
};
const SearchBar = ({
options,
placeholder = "Search here..." ,
} : SearchCompleteProps ) => {
const [ open , setOpen ] = useState ( false );
const [ inputValue , setInputValue ] = useState ( "" );
const inputRef = useRef < HTMLInputElement >( null );
const [ activeIndex , setActiveIndex ] = useState < number >( - 1 );
const handleBlur = useCallback (() => {
// Delay closing to allow click event to register
setTimeout (() => setOpen ( false ), 200 );
}, []);
const handleInputChange = useCallback (
( event : React . ChangeEvent < HTMLInputElement >) => {
const value = event.target.value;
setInputValue (value);
setOpen ( true );
},
[updateFilteredOptions],
);
const handleKeyDown = useCallback (
( event : KeyboardEvent < HTMLInputElement >) => {
if ( ! inputRef.current) return ;
if ( ! isOpen) setOpen ( true );
if (event.key === "ArrowDown" && filteredOptions. length > 0 ) {
event. preventDefault ();
setActiveIndex (( prevIndex ) =>
prevIndex < filteredOptions. length - 1 ? prevIndex + 1 : prevIndex,
);
}
if (event.key === "ArrowUp" && filteredOptions. length > 0 ) {
event. preventDefault ();
setActiveIndex (( prevIndex ) =>
prevIndex > 0 ? prevIndex - 1 : prevIndex,
);
}
if (event.key === "Enter" && activeIndex >= 0 ) {
const selectedOption = filteredOptions[activeIndex];
if (selectedOption) {
setInputValue (selectedOption);
setOpen ( false );
onSelect ?.(selectedOption);
}
}
if (event.key === "Escape" ) {
setOpen ( false );
inputRef.current. blur ();
}
},
[isOpen, filteredOptions, activeIndex, onSelect],
);
return (
< div className = "relative" >
< input
type = "text"
placeholder = {placeholder}
onFocus = {() => {
setOpen ( true );
}}
onKeyDown = {handleKeyDown}
onChange = {handleInputChange}
/>
< div className = { cn ( "absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center hidden bg-white shadow-lg rounded-md" ,{
"block" :open
})}>
{options. map (( option , index ) => (
< div
key = {option}
>
{option}
</ div >
))}
</ div >
</ div >
)
}
Final Code : Adding Search Filters using regex and screen readers accessibility
Finally, we’ll enhance the search functionality by adding regex-based filters. This allows for more flexible searches and makes it easier to find specific items. We’ll also make sure the search bar is accessible to screen readers, so it’s usable by everyone.
import React, { KeyboardEvent, useCallback, useRef, useState } from 'react' ;
import { cn } from '../../lib/utils' ;
import { Input } from './input' ;
export type SearchCompleteProps = {
options : string [];
placeholder ?: string ;
noResultsText ?: string ;
onSelect ?: ( value : string ) => void ;
};
export default function SearchComplete ({
options ,
placeholder = 'Search here...' ,
noResultsText = 'No results found' ,
onSelect ,
} : SearchCompleteProps ) {
const inputRef = useRef < HTMLInputElement >( null );
const [ isOpen , setOpen ] = useState ( false );
const [ inputValue , setInputValue ] = useState ( '' );
const [ filteredOptions , setFilteredOptions ] = useState < string []>(options);
const [ activeIndex , setActiveIndex ] = useState < number >( - 1 );
// Debounced input change handler
const debounce = ( func : ( value : string ) => void , delay : number ) => {
let timer : NodeJS . Timeout ;
return ( value : string ) => {
clearTimeout (timer);
timer = setTimeout (() => func (value), delay);
};
};
const updateFilteredOptions = useCallback (
( value : string ) => {
if (value. trim () === '' ) {
setFilteredOptions (options);
} else {
const pattern = value
. split ( '' )
. map ( letter => `[${ letter }]` )
. join ( '.*' );
const regex = new RegExp (pattern, 'i' );
const filtered = options. filter ( option => regex. test (option));
setFilteredOptions (filtered);
}
setActiveIndex ( - 1 );
},
[options]
);
const debouncedUpdateFilteredOptions = debounce (updateFilteredOptions, 300 );
const handleKeyDown = useCallback (
( event : KeyboardEvent < HTMLInputElement >) => {
if ( ! inputRef.current) return ;
if ( ! isOpen) setOpen ( true );
if (event.key === 'ArrowDown' && filteredOptions. length > 0 ) {
event. preventDefault ();
setActiveIndex ( prevIndex =>
prevIndex < filteredOptions. length - 1 ? prevIndex + 1 : prevIndex
);
}
if (event.key === 'ArrowUp' && filteredOptions. length > 0 ) {
event. preventDefault ();
setActiveIndex ( prevIndex => (prevIndex > 0 ? prevIndex - 1 : prevIndex));
}
if (event.key === 'Enter' && activeIndex >= 0 ) {
const selectedOption = filteredOptions[activeIndex];
if (selectedOption) {
setInputValue (selectedOption);
setOpen ( false );
onSelect ?.(selectedOption);
}
}
if (event.key === 'Escape' ) {
setOpen ( false );
inputRef.current. blur ();
}
},
[isOpen, filteredOptions, activeIndex, onSelect]
);
const handleInputChange = useCallback (
( event : React . ChangeEvent < HTMLInputElement >) => {
const value = event.target.value;
setInputValue (value);
debouncedUpdateFilteredOptions (value);
setOpen ( true );
},
[debouncedUpdateFilteredOptions]
);
const handleSelect = ( val : string ) => {
setInputValue (val);
setOpen ( false );
setActiveIndex ( - 1 );
onSelect ?.(val);
};
const handleBlur = useCallback (() => {
// Delay closing to allow click event to register
setTimeout (() => setOpen ( false ), 200 );
}, []);
const handleOptionClick = ( option : string ) => {
handleSelect (option);
inputRef.current?. blur (); // Ensure the input loses focus
};
return (
< div
className = "relative mx-auto max-w-md"
role = "combobox"
aria-expanded = {isOpen}
aria-controls = "search-options"
aria-activedescendant = {filteredOptions[activeIndex]}
>
< Input
type = "text"
placeholder = {placeholder}
ref = {inputRef}
onBlur = {handleBlur}
value = {inputValue}
onFocus = {() => {
setOpen ( true );
updateFilteredOptions (inputValue);
}}
onKeyDown = {handleKeyDown}
onChange = {handleInputChange}
aria-autocomplete = "list"
aria-controls = "search-options"
aria-haspopup = "true"
aria-label = {placeholder}
className = "w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
< div
id = "search-options"
className = { cn (
`absolute left-0 top-12 z-10 mt-1 hidden w-full overflow-hidden rounded-lg border border-gray-300 bg-background shadow-lg transition-all ease-in-out` ,
{
'block animate-in fade-in-60 zoom-in-95' : isOpen,
}
)}
role = "listbox"
>
{filteredOptions. length === 0 ? (
< div className = "p-3 text-center text-gray-500" >{noResultsText}</ div >
) : (
filteredOptions. map (( option , index ) => (
< div
key = {option}
role = "option"
aria-selected = {index === activeIndex}
onMouseOver = {() => setActiveIndex (index)}
onClick = {() => handleOptionClick (option)}
className = { cn ( `cursor-pointer p-3` , {
'bg-muted' : index === activeIndex,
})}
>
{option}
</ div >
))
)}
</ div >
</ div >
);
}