ausbizConsulting
Home/Components/SearchCommand

SearchCommand

A reusable search component with asynchronous search capabilities and keyboard navigation.

React
TypeScript
Shadcn UI
Accessible

Overview

Modern applications often require powerful search interfaces that can handle asynchronous data loading, maintain accessibility standards, and provide an excellent user experience. The SearchCommand component addresses these needs with a reusable, generic search interface.

Built on top of the shadcn/ui command component, SearchCommand adds asynchronous search capabilities, keyboard navigation, loading states, and full TypeScript support for working with any data type.

Basic Search Input

Simple implementation
No loading states
Poor accessibility

Custom UI Components

Highly customizable
Development overhead
Inconsistent behavior

SearchCommand Approach

Type-safe with generics
Async search with loading states
Keyboard navigation

Installation

Install the required shadcn/ui components:

npx shadcn-ui@latest add command popover

Create the SearchCommand component:

'use client'

import * as React from "react"
import { useCallback, useState, useRef } from "react"
import { Check, Loader2 } from 'lucide-react'
import { cn } from "@/lib/utils"
import {
  Command,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export interface SearchCommandProps<T> {
  onSearch: (value: string) => Promise<T[]>
  onItemSelect: (item: T) => void
  getItemId: (item: T) => string
  getItemLabel: (item: T) => string
  placeholder?: string
  noResultsText?: string
}

Usage

The SearchCommand component provides a generic interface for implementing async search with any data type. It handles loading states, keyboard navigation, and maintaining input focus.

import { SearchCommand } from '@/components/products/command-search';

interface User {
  id: string;
  name: string;
  email: string;
}

function UserSearch() {
  // Function to search users
  const searchUsers = async (query: string): Promise<User[]> => {
    // In a real app, you would fetch from an API
    const response = await fetch(`/api/users/search?q=${query}`);
    return response.json();
  };

  // Handle user selection
  const handleUserSelect = (user: User) => {
    console.log('Selected user:', user);
    // Do something with the selected user
  };

  return (
    <div className="w-full max-w-sm">
      <SearchCommand<User>
        onSearch={searchUsers}
        onItemSelect={handleUserSelect}
        getItemId={(user) => user.id}
        getItemLabel={(user) => user.name}
        placeholder="Search users..."
        noResultsText="No users found"
      />
    </div>
  );
}

API Reference

SearchCommand accepts the following props:

PropTypeDescriptionRequired
onSearch(value: string) => Promise<T[]>Async function that returns search resultsYes
onItemSelect(item: T) => voidCallback called when an item is selectedYes
getItemId(item: T) => stringFunction to get a unique identifier from an itemYes
getItemLabel(item: T) => stringFunction to get the display text for an itemYes
placeholderstringPlaceholder text for the search inputNo
noResultsTextstringText to display when no results are foundNo

Examples

Product Search Example

import { SearchCommand } from '@/components/products/command-search';

interface Product {
  sku: string;
  name: string;
  price: number;
  category: string;
}

export function ProductSearch() {
  // Fetch products from API
  const searchProducts = async (query: string): Promise<Product[]> => {
    if (!query) return [];
    
    // In a real app, you would make an API call
    const response = await fetch(`/api/products/search?q=${query}`);
    return response.json();
  };

  const handleProductSelect = (product: Product) => {
    console.log(`Selected: ${product.name} (${product.sku})`);
    // Navigate to product page or show product details
    window.location.href = `/products/${product.sku}`;
  };

  return (
    <div className="w-full max-w-md mx-auto">
      <label htmlFor="product-search" className="text-sm font-medium block mb-2">
        Find a product
      </label>
      <SearchCommand<Product>
        onSearch={searchProducts}
        onItemSelect={handleProductSelect}
        getItemId={(product) => product.sku}
        getItemLabel={(product) => `${product.name} - ${product.price.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`}
        placeholder="Search products by name..."
        noResultsText="No products found matching your search"
      />
    </div>
  );
}

Ready to enhance your search experience?

Try SearchCommand today and provide your users with an accessible, powerful search interface.

AusBiz Consulting