Skip to main content
DocSearch allows you to replace the default hit and footer rendering with custom React components, giving you complete control over how search results appear.

Custom Hit Component

The hitComponent prop lets you render each search result with your own component.

Basic Custom Hit

import { DocSearch } from '@docsearch/react';

function CustomHit({ hit, children }) {
  return (
    <a href={hit.url} className="custom-hit">
      {children}
    </a>
  );
}

<DocSearch
  appId="YOUR_APP_ID"
  apiKey="YOUR_SEARCH_API_KEY"
  indexName="documentation"
  hitComponent={CustomHit}
/>

Props Passed to hitComponent

interface HitComponentProps {
  hit: InternalDocSearchHit | StoredDocSearchHit;
  children: React.ReactNode;
}
The children prop contains the default DocSearch hit content structure. You can:
  • Wrap it in a custom container
  • Replace it entirely with your own markup
  • Add additional elements around it

Complete Custom Hit Example

Here’s a fully customized hit that replaces the default content:
function BrandedHit({ hit }) {
  return (
    <a
      href={hit.url}
      style={{
        display: 'block',
        padding: '12px 16px',
        textDecoration: 'none',
        borderRadius: '8px',
        transition: 'background 0.2s'
      }}
    >
      <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
        {/* Custom icon based on hit type */}
        <div
          style={{
            width: 40,
            height: 40,
            backgroundColor: '#e3f2fd',
            borderRadius: 6,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontWeight: 600,
            color: '#1976d2',
            fontSize: '12px'
          }}
        >
          {hit.type?.toUpperCase?.().slice(0, 3) || 'DOC'}
        </div>

        {/* Custom content layout */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div
            style={{
              fontWeight: 600,
              fontSize: '14px',
              color: '#1a1a1a',
              marginBottom: '4px',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              textOverflow: 'ellipsis'
            }}
          >
            {hit.hierarchy?.lvl1 || 'Untitled'}
          </div>

          {hit.hierarchy?.lvl2 && (
            <div
              style={{
                fontSize: '12px',
                color: '#666',
                whiteSpace: 'nowrap',
                overflow: 'hidden',
                textOverflow: 'ellipsis'
              }}
            >
              {hit.hierarchy.lvl2}
            </div>
          )}

          {hit.content && (
            <div
              style={{
                fontSize: '12px',
                color: '#888',
                marginTop: 4,
                display: '-webkit-box',
                WebkitLineClamp: 2,
                WebkitBoxOrient: 'vertical',
                overflow: 'hidden'
              }}
              dangerouslySetInnerHTML={{ __html: hit._snippetResult?.content?.value || hit.content }}
            />
          )}
        </div>

        {/* Custom badge */}
        {hit.__autocomplete_indexName && (
          <div
            style={{
              padding: '2px 8px',
              fontSize: '10px',
              fontWeight: 600,
              backgroundColor: '#f0f0f0',
              borderRadius: '4px',
              textTransform: 'uppercase'
            }}
          >
            {hit.__autocomplete_indexName}
          </div>
        )}
      </div>
    </a>
  );
}

<DocSearch
  appId="YOUR_APP_ID"
  apiKey="YOUR_SEARCH_API_KEY"
  indexName="documentation"
  hitComponent={BrandedHit}
/>
When you access hit properties like _snippetResult or _highlightResult, you get the highlighted/snippeted version from Algolia with HTML markup. Use dangerouslySetInnerHTML to render it.
To open search results in new tabs, combine a custom hitComponent with the navigator prop:
function HitWithNewTab({ hit, children }) {
  return (
    <a href={hit.url} target="_blank" rel="noopener noreferrer">
      {children}
    </a>
  );
}

const newTabNavigator = {
  navigate: ({ itemUrl }) => window.open(itemUrl, '_blank'),
  navigateNewTab: ({ itemUrl }) => window.open(itemUrl, '_blank'),
  navigateNewWindow: ({ itemUrl }) => window.open(itemUrl, '_blank'),
};

<DocSearch
  appId="YOUR_APP_ID"
  apiKey="YOUR_SEARCH_API_KEY"
  indexName="documentation"
  hitComponent={HitWithNewTab}
  navigator={newTabNavigator}
/>
Using target="_blank" in the hitComponent alone only works for mouse clicks. Keyboard navigation (arrow keys + Enter) requires the navigator prop to open links in new tabs consistently.

Using the Default Children

If you want to keep the default hit content but add wrapper elements:
function HitWithBadge({ hit, children }) {
  const isNew = Date.now() - new Date(hit.published_at).getTime() < 7 * 24 * 60 * 60 * 1000;
  
  return (
    <a href={hit.url} style={{ position: 'relative' }}>
      {isNew && (
        <span
          style={{
            position: 'absolute',
            top: 8,
            right: 8,
            backgroundColor: '#22c55e',
            color: 'white',
            padding: '2px 6px',
            borderRadius: 4,
            fontSize: 10,
            fontWeight: 600
          }}
        >
          NEW
        </span>
      )}
      {children}
    </a>
  );
}
The resultsFooterComponent allows you to render custom content at the bottom of the search results panel.
function CustomFooter({ state }) {
  return (
    <div style={{ padding: '16px', borderTop: '1px solid #e5e7eb' }}>
      <p style={{ margin: 0, fontSize: '12px', color: '#666' }}>
        Found {state.context.nbHits || 0} results
      </p>
    </div>
  );
}

<DocSearch
  appId="YOUR_APP_ID"
  apiKey="YOUR_SEARCH_API_KEY"
  indexName="documentation"
  resultsFooterComponent={CustomFooter}
/>

Props Passed to resultsFooterComponent

interface FooterComponentProps {
  state: AutocompleteState<InternalDocSearchHit>;
}
The state object contains:
  • state.query: Current search query
  • state.context.nbHits: Total number of results
  • state.collections: Array of result collections
  • state.status: Current search status (‘idle’, ‘loading’, ‘stalled’, ‘error’)
function AdvancedFooter({ state }) {
  const hasResults = state.collections.some(
    (collection) => collection.items.length > 0
  );

  if (!hasResults) {
    return null; // Don't show footer when no results
  }

  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '12px 16px',
        borderTop: '1px solid #e5e7eb',
        backgroundColor: '#f9fafb'
      }}
    >
      <div style={{ fontSize: '12px', color: '#666' }}>
        {state.context.nbHits || 0} results for
        <strong style={{ marginLeft: 4 }}>"{state.query}"</strong>
      </div>

      <a
        href={`/search?q=${encodeURIComponent(state.query)}`}
        style={{
          fontSize: '12px',
          color: '#2563eb',
          textDecoration: 'none',
          fontWeight: 500
        }}
        onClick={(e) => {
          e.stopPropagation();
        }}
      >
        View all results →
      </a>
    </div>
  );
}

<DocSearch
  appId="YOUR_APP_ID"
  apiKey="YOUR_SEARCH_API_KEY"
  indexName="documentation"
  resultsFooterComponent={AdvancedFooter}
/>
function ConditionalFooter({ state }) {
  const totalHits = state.context.nbHits || 0;
  
  if (state.status === 'loading') {
    return (
      <div style={{ padding: '16px', textAlign: 'center' }}>
        <div className="loading-spinner" />
        <p>Searching...</p>
      </div>
    );
  }

  if (state.status === 'error') {
    return (
      <div style={{ padding: '16px', color: '#dc2626' }}>
        <p>⚠️ Search error. Please try again.</p>
      </div>
    );
  }

  if (totalHits === 0) {
    return null; // No footer for empty results
  }

  return (
    <div style={{ padding: '16px' }}>
      <p>Showing top results from {totalHits} total matches</p>
    </div>
  );
}

TypeScript Support

When using TypeScript, you can import the proper types:
import type { JSX } from 'react';
import type { AutocompleteState } from '@algolia/autocomplete-core';
import type { InternalDocSearchHit, StoredDocSearchHit } from '@docsearch/react';

interface CustomHitProps {
  hit: InternalDocSearchHit | StoredDocSearchHit;
  children: React.ReactNode;
}

function CustomHit({ hit, children }: CustomHitProps): JSX.Element {
  return (
    <a href={hit.url}>
      {children}
    </a>
  );
}

interface CustomFooterProps {
  state: AutocompleteState<InternalDocSearchHit>;
}

function CustomFooter({ state }: CustomFooterProps): JSX.Element | null {
  return (
    <div>
      Found {state.context.nbHits || 0} results
    </div>
  );
}

Styling Tips

CSS Classes

The default DocSearch structure uses classes like DocSearch-Hit, DocSearch-Hit-Container, etc. You can target these in your custom components or replace them entirely.

Theme Integration

Use inline styles or CSS classes that match your site’s design system. The theme prop controls modal colors but doesn’t affect custom component styles.

Responsive Design

Ensure your custom components work well on mobile devices where the modal is full-screen.

Complete Example

import { DocSearch } from '@docsearch/react';
import '@docsearch/css';

function CustomHit({ hit, children }) {
  return (
    <a
      href={hit.url}
      className="custom-hit"
      data-hit-type={hit.type}
    >
      <div className="hit-icon">
        {getIconForType(hit.type)}
      </div>
      <div className="hit-content">
        {children}
      </div>
    </a>
  );
}

function CustomFooter({ state }) {
  const totalHits = state.context.nbHits || 0;
  
  if (totalHits === 0) return null;
  
  return (
    <div className="custom-footer">
      <span>{totalHits} results</span>
      <a href={`/search?q=${state.query}`}>View all →</a>
    </div>
  );
}

function App() {
  return (
    <DocSearch
      appId="YOUR_APP_ID"
      apiKey="YOUR_SEARCH_API_KEY"
      indexName="documentation"
      hitComponent={CustomHit}
      resultsFooterComponent={CustomFooter}
    />
  );
}

Build docs developers (and LLMs) love