Part 1 : Building a Google-Style Search Component in React

Date - 09/02/2025

Building a search component that behaves like Google or Facebook might seem straightforward at first, but there are several UX nuances that make it challenging. In this blog, I’ll walk through creating a React search component with proper keyboard navigation and focus management.

Try It Out

Try typing “mission” and navigate with arrow keys

The Challenge

When building search autocomplete, you need to handle:

Initial Approach: Using Radix Popover

My first instinct was to use Radix UI’s Popover component:

<Popover open={open} onOpenChange={setOpen}>
  <PopoverAnchor>
    <Input />
  </PopoverAnchor>
  <PopoverContent>
    {/* Search results */}
  </PopoverContent>
</Popover>

The Problem: Radix Popover automatically manages focus, moving it away from the input when opened. This breaks the search experience because users expect to keep typing while seeing results.

The Key Insight: Google vs Facebook Behavior

There are actually two different UX patterns:

Google’s Approach

Facebook’s Approach

I chose Google’s approach for better discoverability.

The Solution: Custom Dropdown

After struggling with Radix’s focus management, I switched to a simple absolutely positioned div:

return (
  <div ref={containerRef} className='w-[40vw] relative'>
    <Input 
      value={inputValue} 
      onChange={onSearchInputChange} 
      onKeyDown={handleKeyDown} 
    />
    {open && data?.results.length && (
      <div className='absolute top-full left-0 right-0 z-50 mt-2 bg-white border shadow-lg'>
        {data.results.map((result, index) => (
          <p className={`p-2 cursor-pointer ${
            selectedIndex === index ? 'bg-blue-100' : 'hover:bg-gray-100'
          }`}>
            {result.title}
          </p>
        ))}
      </div>
    )}
  </div>
);

State Management

The component uses several pieces of state:

const [selectedIndex, setSelectedIndex] = useState(-1); // Which item is highlighted
const [inputValue, setInputValue] = useState('');       // What's shown in input
const [open, setOpen] = useState(false);               // Is dropdown visible

Keyboard Navigation Logic

The tricky part is handling different key behaviors:

const handleKeyDown = (e) => {
  if (!open || !data?.results.length) return;
  
  if (e.key === 'ArrowDown') {
    e.preventDefault();
    // At last item? Go back to original search
    if (selectedIndex === data.results.length - 1) {
      setSelectedIndex(-1);
      setInputValue(search); // Original search term
      return;
    }
    // Otherwise move down
    const newIndex = selectedIndex + 1;
    setSelectedIndex(newIndex);
    setInputValue(data.results[newIndex].title);
  }
  
  // Similar logic for ArrowUp...
  
  if (e.key === 'Escape') {
    setOpen(false);
    setInputValue(search); // Reset to original
  }
};

The Focus Management Secret

The crucial insight: Never change focus from the input. The visual highlighting is achieved purely through CSS and state, not actual DOM focus:

className={`p-2 cursor-pointer ${
  selectedIndex === index ? 'bg-blue-100' : 'hover:bg-gray-100'
}`}

No tabIndex, no .focus() calls on the results. The input maintains focus throughout the entire interaction.

Debounced API Calls

Using useDebounce from @uidotdev/usehooks to prevent API spam:

const debouncedSearchTerm = useDebounce(search, 300);
const { data } = useSWR(
  debouncedSearchTerm ? `api/search?q=${debouncedSearchTerm}` : null,
  fetcher
);

Click Outside Support

Using useClickAway hook for clean closing behavior:

const containerRef = useClickAway(() => {
  setOpen(false);
  setSelectedIndex(-1);
});

Final Implementation

The complete component handles:

Key Takeaways

  1. Don’t fight the framework: When Radix Popover’s focus management didn’t fit, I switched to a simpler solution
  2. Focus is everything: Keep focus on the input and use visual styling for “selection”
  3. Study the UX: Google and Facebook have different patterns - choose what fits your use case
  4. Debounce is essential: Don’t hammer your API with every keystroke

The final result feels natural and responsive, just like the search experiences users are familiar with.

Code

You can find the complete implementation in the demo above. The key files are:

Try typing “mission” in the search above and use your arrow keys to see it in action!