React examples
These examples build on the React getting started guide. Replace all placeholder credentials before using them.
Search multiple indices
Pass strings or per-index objects. Results follow the array order.
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={[
{
name: 'product_docs',
searchParameters: {
facetFilters: ['language:en'],
},
},
'api_reference',
]}
/>
Add facet filters
Configure the attributes for faceting in Algolia, then expose up to five controls:
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
facets={[
{ key: 'language', label: 'Language' },
{ key: 'version', label: 'Version' },
]}
/>
DocSearch loads available values from all configured indices. When a user selects values, DocSearch replaces each index's configured facetFilters entries for that facet key with the selection and keeps entries for other keys. Users can select multiple values per facet. DocSearch combines them with OR, for example [['language:en', 'language:fr'], 'version:v2'].
Show a result badge
Retrieve the property and pass its path to resultBadgeKey:
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={[
{
name: 'product_docs',
searchParameters: {
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
'version',
],
},
},
]}
resultBadgeKey="version"
translations={{
modal: {
resultsScreen: {
resultBadgeLabelText: 'Version',
},
},
}}
/>
Nested paths such as hierarchy.lvl1 and tags[2] also work.
Customize result links
Use hitComponent to change the link while preserving DocSearch's result content:
function SearchHit({ hit, children }) {
return (
<a
href={hit.url}
data-index={hit.__autocomplete_indexName}
onClick={() => console.info('Opened result', hit.objectID)}
>
{children}
</a>
);
}
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
hitComponent={SearchHit}
/>;
Add a results footer
Use the Autocomplete state to show query-level information:
function ResultsFooter({ state }) {
const count = state.context.nbHits ?? 0;
return <p>{count} matching records</p>;
}
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
resultsFooterComponent={ResultsFooter}
/>;
Transform results
Return the hits in the order you want DocSearch to group and render them:
<DocSearch
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
transformItems={(items) =>
items.filter((item) => item.hierarchy.lvl0 !== 'Archived')
}
/>
Open search from another control
import { useRef } from 'react';
import { DocSearch, type DocSearchRef } from '@docsearch/react';
function Search() {
const searchRef = useRef<DocSearchRef>(null);
return (
<>
<button type="button" onClick={() => searchRef.current?.open()}>
Search documentation
</button>
<DocSearch
ref={searchRef}
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
/>
</>
);
}
Add Agent Studio search parameters
Use DocSearchAI. Key Ask AI search parameters by index name:
<DocSearchAI
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
askAi={{
assistantId: 'YOUR_ASSISTANT_ID',
searchParameters: {
product_docs: {
filters: 'language:en AND version:v5',
attributesToRetrieve: ['title', 'content', 'url'],
restrictSearchableAttributes: ['title', 'content'],
distinct: true,
},
},
}}
/>
See Get started with Agent Studio before configuring the component.
Add dynamic Agent Studio indices
Pass the index names Agent Studio should search for this request:
const askAi = {
agentId: 'YOUR_ASSISTANT_ID',
indices: ['product_docs', 'api_reference'],
searchParameters: {
product_docs: {
filters: 'language:en',
},
},
};
<DocSearchAI
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs', 'api_reference']}
askAi={askAi}
/>;
Define stable objects outside the component, or memoize them, when they contain tools.
Add prompt suggestions
Show matching prompts next to keyword results:
<DocSearchAI
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
askAi={{
assistantId: 'YOUR_ASSISTANT_ID',
suggestedQuestions: true,
promptSuggestions: {
indexName: 'docsearch_prompt_suggestions',
hitsPerPage: 3,
},
}}
/>
suggestedQuestions shows published assistant questions on a new conversation. promptSuggestions searches your prompt index while the user types a keyword query.
Route Ask AI to another view
Return true from interceptAskAiEvent after handling the message:
<DocSearchAI
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
indices={['product_docs']}
askAi="YOUR_ASSISTANT_ID"
interceptAskAiEvent={(initialMessage) => {
openCustomAssistant(initialMessage);
return true;
}}
/>
For the supported modal and Sidepanel integration, use hybrid mode.
Lazy-load the Sidepanel
Import the trigger from @docsearch/react/sidepanelButton, then dynamically import Sidepanel from @docsearch/react/sidepanelPanel. The button entry excludes the panel and its Ask AI dependencies. Dynamically importing the panel entry defers those dependencies until needed.
Don't statically import the button from @docsearch/react/sidepanel when you need this boundary. Tree shaking depends on the bundler: a production Bun build with both a static button import and a dynamic panel import from the shared @docsearch/react/sidepanel entry includes the AI SDK and Markdown renderer in the initial chunks. Using the dedicated sidepanelButton/sidepanelPanel entries keeps those dependencies deferred and avoids bundling the button twice.
import { lazy, Suspense, useRef, useState } from 'react';
import type { JSX } from 'react';
import { SidepanelButton } from '@docsearch/react/sidepanelButton';
import '@docsearch/css/dist/sidepanel.css';
const loadSidepanel = () => import('@docsearch/react/sidepanelPanel');
const LazySidepanel = lazy(() =>
loadSidepanel().then(({ Sidepanel }) => ({ default: Sidepanel }))
);
const keyboardShortcuts = { 'Ctrl/Cmd+I': false };
function preloadSidepanel() {
// Let activation retry if this speculative preload fails.
loadSidepanel().catch(() => {});
}
export function LazyAssistant(): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const trigger = useRef<HTMLButtonElement | null>(null);
return (
<>
<SidepanelButton
variant="inline"
keyboardShortcuts={keyboardShortcuts}
aria-expanded={isOpen}
onMouseEnter={preloadSidepanel}
onFocus={preloadSidepanel}
onTouchStart={preloadSidepanel}
onClick={(event) => {
trigger.current = event.currentTarget;
setIsOpen(true);
}}
/>
{isOpen && (
<Suspense fallback={<output>Loading assistant…</output>}>
<LazySidepanel
appId="YOUR_APP_ID"
apiKey="YOUR_SEARCH_API_KEY"
agentId="YOUR_AGENT_ID"
isOpen={isOpen}
onOpen={() => setIsOpen(true)}
onClose={() => {
setIsOpen(false);
trigger.current?.focus();
}}
keyboardShortcuts={keyboardShortcuts}
/>
</Suspense>
)}
</>
);
}
Hover, focus, and touch start preload the module without opening the panel. Activation also loads it if preloading hasn't finished. This example disables the global shortcut because the panel's keyboard listener isn't mounted before activation, and unmounts the panel on close. To preserve an in-progress conversation, keep it mounted after the first activation and control visibility with isOpen.
For provider-based composition, @docsearch/sidepanel/button also uses the lightweight trigger entry. Keep the panel mounted when you need provider refs, keyboard shortcuts, or hybrid handoff before a user activates the trigger.