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.
Opening Links in New Tabs
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 }
/>
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 }
/>
);
}