Skip to content

HeadlessSearchInput Component

A powerful, headless React component for searching within collections. This is the primary search component that provides complete control over rendering and behavior. It supports both single collection searches and multiple collection searches using a single component.

Import

typescript
import { HeadlessSearchInput } from 'typesense-search-plugin'
// or
import HeadlessSearchInput from 'typesense-search-plugin/components/HeadlessSearchInput'

Props

PropTypeDefaultDescription
baseUrlstringRequiredBase URL of your Payload CMS instance
collectionstring-Collection name to search in (single collection)
collectionsstring[]-Collections to search in (multi-collection)
classNamestring''CSS class for the container
debounceMsnumber300Debounce delay in milliseconds
enableSuggestionsbooleantrueEnable search suggestions
errorClassNamestring''CSS class for error state
inputClassNamestring''CSS class for the input element
inputWrapperClassNamestring''CSS class for input wrapper
loadingClassNamestring''CSS class for loading state
minQueryLengthnumber2Minimum query length before searching
noResultsClassNamestring''CSS class for no results state
onResultClick(result: SearchResult<T>) => voidundefinedCallback when a result is clicked
onResults(results: SearchResponse<T>) => voidundefinedCallback when search results are received
onSearch(query: string, results: SearchResponse<T>) => voidundefinedCallback when search is performed
perPagenumber10Number of results per page
placeholderstring'Search...'Input placeholder text
renderError(error: string) => React.ReactNodeundefinedCustom error renderer
renderInput(props: InputProps) => React.ReactNodeundefinedCustom input renderer
renderLoading() => React.ReactNodeundefinedCustom loading renderer
renderNoResults(query: string) => React.ReactNodeundefinedCustom no results renderer
renderResult(result: SearchResult<T>, index: number) => React.ReactNodeundefinedCustom result renderer
renderResultsHeader(found: number, searchTime: number) => React.ReactNodeundefinedCustom results header renderer
resultItemClassNamestring''CSS class for individual result items
resultsClassNamestring''CSS class for the results container
resultsHeaderClassNamestring''CSS class for results header
resultsListClassNamestring''CSS class for results list
showLoadingbooleantrueShow loading indicator
showResultCountbooleantrueShow result count
showSearchTimebooleantrueShow search time
maxResultsnumberundefinedMaximum number of results to display
perPagenumber10Number of results per page
onError(error: string) => voidundefinedCallback when search fails
onLoading(loading: boolean) => voidundefinedCallback when loading state changes

Note: You can provide either collection (single), collections (multiple), or neither (all collections). The component will automatically choose the most efficient API endpoint.

Basic Usage - Single Collection

tsx
import { HeadlessSearchInput } from 'typesense-search-plugin'

function PostSearch() {
  return (
    <HeadlessSearchInput
      baseUrl="http://localhost:3000"
      collection="posts"
      placeholder="Search posts..."
    />
  )
}

The HeadlessSearchInput component supports multiple collection search patterns:

Option 1: Multiple Collections with Single Component

tsx
function MultiCollectionSearch() {
  return (
    <div className="multi-collection-search">
      {/* Search specific collections */}
      <HeadlessSearchInput
        baseUrl="http://localhost:3000"
        collections={['posts', 'products']}
        placeholder="Search posts & products..."
        onResultClick={(result) => console.log('Result clicked:', result)}
      />

      {/* Search all collections */}
      <HeadlessSearchInput
        baseUrl="http://localhost:3000"
        placeholder="Search all collections..."
        onResultClick={(result) => console.log('Result clicked:', result)}
      />
    </div>
  )
}

Option 2: Multiple Collection-Specific Inputs

tsx
function MultiCollectionSearch() {
  return (
    <div className="multi-collection-search">
      <div className="search-section">
        <h3>Posts</h3>
        <HeadlessSearchInput
          baseUrl="http://localhost:3000"
          collection="posts"
          placeholder="Search posts..."
          onResultClick={(result) => console.log('Post clicked:', result)}
        />
      </div>

      <div className="search-section">
        <h3>Products</h3>
        <HeadlessSearchInput
          baseUrl="http://localhost:3000"
          collection="products"
          placeholder="Search products..."
          onResultClick={(result) => console.log('Product clicked:', result)}
        />
      </div>
    </div>
  )
}

Option 3: Custom Universal Search Implementation

tsx
import { useState, useCallback } from 'react'

function UniversalSearch() {
  const [results, setResults] = useState(null)
  const [loading, setLoading] = useState(false)

  const handleUniversalSearch = useCallback(async (query: string) => {
    if (query.length < 2) {
      setResults(null)
      return
    }

    setLoading(true)
    try {
      // Use the universal search endpoint that searches all collections
      const response = await fetch(
        `http://localhost:3000/api/search?q=${encodeURIComponent(query)}&per_page=10`,
      )
      if (response.ok) {
        const searchResults = await response.json()
        setResults(searchResults)
      }
    } catch (error) {
      console.error('Search failed:', error)
    } finally {
      setLoading(false)
    }
  }, [])

  const renderUniversalResults = () => {
    if (!results) return null

    return (
      <div className="universal-results">
        <div className="results-header">
          Found {results.found} result{results.found !== 1 ? 's' : ''}
          {results.search_time_ms && ` in ${results.search_time_ms}ms`}
        </div>

        {results.hits?.map((hit, index) => (
          <div
            key={index}
            className="result-item"
            onClick={() => console.log('Result clicked:', hit)}
          >
            <div className="result-header">
              <span className="collection-icon">{hit.icon || '📄'}</span>
              <span className="collection-name">{hit.collection}</span>
            </div>
            <div className="result-title">
              {hit.document?.title || hit.document?.name || 'Untitled'}
            </div>
            <div className="result-description">
              {hit.document?.description || hit.document?.shortDescription || 'No description'}
            </div>
          </div>
        ))}
      </div>
    )
  }

  return (
    <div className="universal-search">
      <input
        type="text"
        placeholder="Search all collections..."
        onChange={(e) => handleUniversalSearch(e.target.value)}
      />
      {loading && <div>Searching...</div>}
      {renderUniversalResults()}
    </div>
  )
}

Advanced Usage with Custom Rendering

tsx
import { HeadlessSearchInput } from 'typesense-search-plugin'

function CustomPostSearch() {
  const customRenderResult = (result, index) => (
    <div
      key={result.id}
      className="custom-result-item"
      data-result-item
      onClick={() => console.log('Clicked:', result)}
    >
      <h3>{result.title}</h3>
      <p>{result.document.content}</p>
      <span className="match-score">{result.text_match}% match</span>
    </div>
  )

  const customRenderInput = (props) => (
    <div className="search-input-wrapper">
      <input {...props} />
      <span className="search-icon">🔍</span>
    </div>
  )

  return (
    <HeadlessSearchInput
      baseUrl="http://localhost:3000"
      collection="posts"
      className="my-search-container"
      inputClassName="my-input"
      resultsClassName="my-results"
      resultItemClassName="my-result-item"
      placeholder="Search posts..."
      renderInput={customRenderInput}
      renderResult={customRenderResult}
      debounceMs={500}
      minQueryLength={3}
      perPage={20}
    />
  )
}

Custom Input Rendering

The renderInput prop gives you complete control over the input element:

tsx
const customInputRenderer = (props) => (
  <div className="search-input-container">
    <input {...props} />
    <button
      type="button"
      onClick={() => props.onChange({ target: { value: '' } } as any)}
    >
      Clear
    </button>
  </div>
)

<HeadlessSearchInput
  baseUrl="http://localhost:3000"
  collection="posts"
  renderInput={customInputRenderer}
/>

Keyboard Navigation

The component includes built-in keyboard navigation:

  • Arrow Down: Move to next result
  • Arrow Up: Move to previous result
  • Enter: Select current result
  • Escape: Close results
tsx
// Results are automatically focusable with data-result-item attribute
const customRenderResult = (result, index) => (
  <div
    data-result-item
    tabIndex={0}
    onKeyDown={(e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault()
        // Handle selection
      }
    }}
  >
    {result.title}
  </div>
)

Event Handlers

onResultClick

tsx
const handleResultClick = (result) => {
  // Navigate to result page
  router.push(`/posts/${result.document.id}`)
}

onResults

tsx
const handleResults = (results) => {
  // Track search analytics
  analytics.track('search_results', {
    query: results.request_params.q,
    found: results.found,
    searchTime: results.search_time_ms,
  })
}

onSearch

tsx
const handleSearch = (query, results) => {
  console.log(`Searching for "${query}" found ${results.found} results`)
}

Styling with CSS Modules

The component uses CSS modules. You can override styles:

css
/* styles.css */
.my-search-container {
  position: relative;
  width: 100%;
}

.my-input {
  width: 100%;
  padding: 12px;
  border: 2px solid #e1e5e9;
  border-radius: 8px;
  font-size: 16px;
}

.my-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  border: 1px solid #e1e5e9;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  max-height: 400px;
  overflow-y: auto;
  z-index: 1000;
}

.my-result-item {
  padding: 12px;
  border-bottom: 1px solid #f3f4f6;
  cursor: pointer;
  transition: background-color 0.2s;
}

.my-result-item:hover {
  background-color: #f8f9fa;
}

.my-result-item:focus {
  outline: 2px solid #0070f3;
  outline-offset: -2px;
}

TypeScript Support

tsx
import { HeadlessSearchInput, SearchResult, SearchResponse } from 'typesense-search-plugin'

interface PostDocument {
  id: string
  title: string
  content: string
  category: string
  status: 'draft' | 'published'
}

function TypedPostSearch() {
  const handleResultClick = (result: SearchResult<PostDocument>) => {
    // result.document is typed as PostDocument
    console.log(result.document.category)
  }

  return (
    <HeadlessSearchInput<PostDocument>
      baseUrl="http://localhost:3000"
      collection="posts"
      onResultClick={handleResultClick}
    />
  )
}

Performance Optimization

  • Debounced search prevents excessive API calls
  • Efficient re-rendering with React hooks
  • Built-in caching for search results
  • Lazy loading of results

Accessibility

  • ARIA labels for screen readers
  • Keyboard navigation support
  • Focus management
  • Semantic HTML structure
  • Proper role attributes

Released under the MIT License.