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.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:

// 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:

// 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.