Back to posts
Building a Spotlight Search Feature

Building a Spotlight Search Feature

Matan Shaviro / June 15, 2025

In this post, we'll explore how to build a MacOS-style Spotlight Search feature in React, focusing on component architecture, state management, and React best practices. We'll see how to create a maintainable and scalable search implementation that can be easily integrated into any React application.

The Feature Overview

The Spotlight Search feature allows users to:

  • Quick-search through navigation items
  • Navigate using keyboard shortcuts
  • Access both main routes and sub-routes
  • Get instant visual feedback

Architecture and Component Structure

Our implementation follows a clear separation of concerns with the following structure:

spotlight-search/
├── components/
   ├── search-dialog/
   ├── empty-state/
   ├── result-item/
   └── result-group/
├── hooks/
   ├── use-search.ts
   ├── use-keyboard-navigation.ts
   └── use-spotlight-search-dialog.ts
├── spotlight-search.tsx
└── spotlight-search.context.tsx

Key Implementation Patterns

1. Context-Based State Management

We use React Context to manage the search state and make it available throughout the component tree:

Spotlight Search Contexttypescript
// spotlight-search.context.tsx
export const SpotlightSearchProvider = ({ children }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [selectedIndex, setSelectedIndex] = useState(0);

  const value = {
      isOpen,
      searchQuery,
      selectedIndex,
      openSpotlightSearch: useCallback(() => {
          setIsOpen(true);
          setSearchQuery('');
          setSelectedIndex(0);
      }, []),
      closeSpotlightSearch: useCallback(() => {
          setIsOpen(false);
          setSearchQuery('');
          setSelectedIndex(0);
      }, []),
  };

  return <SpotlightSearchContext.Provider value={value}>{children}</SpotlightSearchContext.Provider>;
};

2. Custom Hooks for Logic Separation

We separate complex logic into custom hooks:

useSearch Custom Hooktypescript
// use-search.ts
export const useSearch = (searchQuery: string) => {
const navigationItems = useTransformData()

return useMemo(() => {
  if (!searchQuery.trim()) return []

  const normalizedQuery = searchQuery.toLowerCase().trim()
  return optimizedSearchableItems.reduce((acc, item) => {
    if (
      acc.length < MAX_SEARCH_RESULTS &&
      item._searchText.includes(normalizedQuery)
    ) {
      const { _searchText, ...cleanItem } = item
      acc.push(cleanItem)
    }
    return acc
  }, [])
}, [searchQuery, optimizedSearchableItems])
}

React Best Practices Demonstrated

  1. Proper TypeScript Usage

    • Using PropsWithChildren for better type safety
    • Defining clear interfaces for component props
    • Utilizing proper type inference
  2. Performance Optimizations

    • Using useMemo for expensive computations
    • Implementing useCallback for stable function references
    • Optimizing search with single-pass reduce operations
  3. Component Composition

    • Clear separation of concerns
    • Reusable and focused components
    • Proper prop drilling avoidance using context
  4. State Management

    • Centralized state using Context
    • Controlled component patterns
    • Predictable state updates

Key Learnings

  1. Abstraction Level

    • The SpotlightSearch component acts as a feature wrapper
    • Context provides a clean API for state management
    • Hooks separate business logic from presentation
  2. Maintainability

    • Clear file structure
    • Focused component responsibilities
    • Easy to test implementation
  3. Scalability

    • Extensible search implementation
    • Modular component architecture
    • Easy to customize and extend

Quick Start Example

Let's look at a minimal implementation using a simple data structure:

Simple Spotlight Search Implementationtypescript
// types.ts
interface SearchItem {
  id: string;
  title: string;
  path: string;
  category?: string;
}

// sample-data.ts
const sampleData: SearchItem[] = [
  { id: '1', title: 'Dashboard', path: '/dashboard', category: 'Main' },
  { id: '2', title: 'User Settings', path: '/settings', category: 'Main' },
  { id: '3', title: 'Profile', path: '/settings/profile', category: 'Settings' },
  { id: '4', title: 'Notifications', path: '/settings/notifications', category: 'Settings' },
];

// simple-spotlight.tsx
import { useState, useCallback } from 'react';

export const SimpleSpotlight = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [query, setQuery] = useState('');

  const filteredItems = sampleData.filter(item =>
      item.title.toLowerCase().includes(query.toLowerCase())
  );

  const handleSelect = (item: SearchItem) => {
      console.log(`Navigate to: ${item.path}`);
      setIsOpen(false);
  };

  return (
      <>
          <button onClick={()=> setIsOpen(true)}>
              Search (Press ⌘ + K)
          </button>

          {isOpen && (
              <div className="spotlight-overlay">
                  <div className="spotlight-modal">
                      <input
                          value={query}
                          onChange={(e)=> setQuery(e.target.value)}
                          placeholder="Search..."
                          autoFocus
                      />
                      <div className="results">
                          {filteredItems.map(item => (
                              <div
                                  key={item.id}
                                  className="result-item"
                                  onClick={()=> handleSelect(item)}
                              >
                                  <span>{item.title}</span>
                                  <span className="category">{item.category}</span>
                              </div>
                          ))}
                      </div>
                  </div>
              </div>
          )}
      </>
  );
};

// Basic styles to get started
const styles= `
.spotlight-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  padding-top: 100px;
}

.spotlight-modal {
  background: white;
  border-radius: 8px;
  width: 500px;
  max-height: 400px;
  overflow: auto;
}

.spotlight-modal input {
  width: 100%;
  padding: 16px;
  border: none;
  border-bottom: 1px solid #eee;
  font-size: 16px;
}

.result-item {
  padding: 12px 16px;
  display: flex;
  justify-content: space-between;
  cursor: pointer;
}

.result-item:hover {
  background: #f5f5f5;
}

.category {
  color: #666;
  font-size: 14px;
}
`;

Conclusion

Building a Spotlight Search feature requires careful consideration of component architecture, state management, and performance. By following React best practices and maintaining a clear separation of concerns, we've created a maintainable and scalable implementation that provides a great user experience.

The complete implementation demonstrates how to:

  • Structure complex features in React
  • Manage state effectively
  • Implement keyboard navigation
  • Handle performance optimization
  • Support feature toggling

This pattern can be adapted for various search implementations while maintaining code quality and user experience.