> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/algolia/docsearch/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Components

> Create custom hit and footer components to match your brand and functionality

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

```jsx theme={null}
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

```typescript theme={null}
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:

```jsx theme={null}
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}
/>
```

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

## Opening Links in New Tabs

To open search results in new tabs, combine a custom `hitComponent` with the `navigator` prop:

```jsx theme={null}
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}
/>
```

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

## Using the Default Children

If you want to keep the default hit content but add wrapper elements:

```jsx theme={null}
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>
  );
}
```

## Custom Results Footer Component

The `resultsFooterComponent` allows you to render custom content at the bottom of the search results panel.

### Basic Footer Component

```jsx theme={null}
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

```typescript theme={null}
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')

## Advanced Footer Example

```jsx theme={null}
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}
/>
```

## Conditional Footer Based on State

```jsx theme={null}
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:

```typescript theme={null}
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

<Card title="CSS Classes" icon="palette">
  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.
</Card>

<Card title="Theme Integration" icon="brush">
  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.
</Card>

<Card title="Responsive Design" icon="mobile">
  Ensure your custom components work well on mobile devices where the modal is full-screen.
</Card>

## Complete Example

```jsx theme={null}
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}
    />
  );
}
```
