Blogs

Customizable Filtered Search Bar Like Google

Search Bar

Demo

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.

tsx

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.

tsx

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.

tsx

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.

tsx

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> ) }

Handling Search Input onChange

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.

tsx

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.

tsx

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.

tsx

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> ); }