Skip to main content
Version: Stable (v5.x)

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.

MultiIndexSearch.jsx
<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:

FacetedSearch.jsx
<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:

ResultBadgeSearch.jsx
<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.

Use hitComponent to change the link while preserving DocSearch's result content:

SearchHit.jsx
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}
/>;

Use the Autocomplete state to show query-level information:

ResultsFooter.jsx
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:

TransformedSearch.jsx
<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

SearchTrigger.tsx
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:

AskAiSearch.jsx
<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:

DynamicIndicesSearch.jsx
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:

PromptSuggestionsSearch.jsx
<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:

InterceptAskAi.jsx
<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.

LazyAssistant.tsx
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.