Help us improve
Share bugs, ideas, or general feedback.
From algolia
Building autocomplete and search-suggestion experiences with the Algolia Autocomplete UI library — distinct from InstantSearch. Covers query suggestions, multi-source autocomplete (suggestions + recent searches + records + redirects + content), the plugin architecture (`@algolia/autocomplete-plugin-*`), keyboard nav, accessibility, theming on shadcn, integration into a Next.js header, and the Query Suggestions index. Use this skill when building the global header search box, a command-palette experience, or any "type ahead and pick" UI. Don't reach for InstantSearch for these — Autocomplete is a different library with a different mental model.
npx claudepluginhub bpainter/composable-dxp-claude-marketplace --plugin algoliaHow this skill is triggered — by the user, by Claude, or both
Slash command
/algolia:algolia-autocompleteThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill puts you in the role of a front-end engineer building a typeahead-and-pick experience. Default posture: **Autocomplete is not a smaller InstantSearch — it's a different library with its own architecture, optimized for the header-search and command-palette pattern.**
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
This skill puts you in the role of a front-end engineer building a typeahead-and-pick experience. Default posture: Autocomplete is not a smaller InstantSearch — it's a different library with its own architecture, optimized for the header-search and command-palette pattern.
The two libraries solve different jobs. InstantSearch builds full SERPs with refinement. Autocomplete builds dropdowns where the user picks one thing and goes. Mixing them on one page is fine; using one for the other's job creates pain.
Pair with algolia-relevance-tuning (the Query Suggestions index needs separate tuning), algolia-search-client (Autocomplete uses the search client under the hood), and algolia-instantsearch-react (the SERP page Autocomplete usually points to).
⌘K) for navigating an app or site.algolia-instantsearch-react.algolia-instantsearch-react.pnpm add @algolia/autocomplete-js \
@algolia/autocomplete-plugin-recent-searches \
@algolia/autocomplete-plugin-query-suggestions \
@algolia/autocomplete-plugin-redirect-url \
algoliasearch
For React-rendered items (recommended on a Next.js site):
pnpm add @algolia/autocomplete-js # the renderer is framework-agnostic; you can use h() or createRoot
(Autocomplete doesn't ship a "React InstantSearch"-style component package. You get the primitive autocomplete() factory and bring your own framework rendering. There's a thin React example pattern below.)
import { autocomplete } from '@algolia/autocomplete-js';
import { algoliasearch } from 'algoliasearch';
const client = algoliasearch(APP_ID, SEARCH_KEY);
autocomplete({
container: '#autocomplete',
placeholder: 'Search the second brain…',
openOnFocus: true,
getSources({ query }) {
return [
{
sourceId: 'articles',
getItems() {
return client
.search({
requests: [{ indexName: 'articles', query, hitsPerPage: 5 }],
})
.then(({ results }) => results[0].hits);
},
templates: {
item({ item, html }) {
return html`<a href="${item.url}">${item.title}</a>`;
},
},
},
];
},
});
That's the bones. Real header search has 4–6 sources stacked.
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js';
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
import { createRedirectUrlPlugin } from '@algolia/autocomplete-plugin-redirect-url';
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: 'composable-dxp-recent-searches',
limit: 5,
});
const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient: client,
indexName: 'articles_query_suggestions',
getSearchParams() {
return recentSearchesPlugin.data!.getAlgoliaSearchParams({ hitsPerPage: 5 });
},
});
const redirectUrlPlugin = createRedirectUrlPlugin();
autocomplete({
container: '#autocomplete',
openOnFocus: true,
plugins: [recentSearchesPlugin, querySuggestionsPlugin, redirectUrlPlugin],
getSources({ query }) {
if (!query) return []; // recent + suggestions handle the empty state via plugins
return [
{
sourceId: 'articles',
getItems() {
return getAlgoliaResults({
searchClient: client,
queries: [
{ indexName: 'articles', params: { query, hitsPerPage: 4, clickAnalytics: true } },
],
});
},
templates: {
header: () => 'Articles',
item: ArticleItem,
noResults: () => 'No articles match.',
},
},
{
sourceId: 'people',
getItems() {
return getAlgoliaResults({
searchClient: client,
queries: [{ indexName: 'people', params: { query, hitsPerPage: 3 } }],
});
},
templates: {
header: () => 'People',
item: PersonItem,
},
},
{
sourceId: 'glossary',
getItems() {
return getAlgoliaResults({
searchClient: client,
queries: [{ indexName: 'glossary_terms', params: { query, hitsPerPage: 3 } }],
});
},
templates: {
header: () => 'Glossary',
item: GlossaryItem,
},
},
];
},
});
Order matters. Most teams do: recent searches → query suggestions → records (most-relevant index first) → redirects (handled by plugin).
The Query Suggestions index is generated from your traffic. You don't write to it; you configure a source index in the Dashboard and Algolia builds the suggestions index nightly.
Setup:
articles. Algolia derives suggestions from queries that produced results.articles_query_suggestions. This is what createQuerySuggestionsPlugin queries.Until enough Insights events flow, the suggestions index is sparse. Wire Insights early.
createLocalStorageRecentSearchesPlugin stores the last N queries the user submitted (locally). Shows them on focus when query is empty. Tracks a query as "submitted" when the user picks an item or hits enter.
For cross-device persistence, swap to a server-backed store. The plugin's API supports a custom data source. Most engagements are fine with LocalStorage.
Server-side: define a query rule in Algolia (algolia-relevance-tuning) that triggers on a query and sets userData: { redirectUrl: '/holiday-sale' }. Client-side: createRedirectUrlPlugin() watches for that and renders a "Press enter to go" hint.
Use case: the user types "holiday" and gets pulled directly to the holiday landing page instead of the SERP.
The templates.item function gets { item, html, components } and returns DOM. The html tagged template is from htm — works with preact. For React, render via createRoot:
import { createRoot } from 'react-dom/client';
autocomplete({
// ...
renderer: { createElement: React.createElement, Fragment: React.Fragment, render: () => {} },
render({ children }, root) {
if (!root._autocompleteRoot) {
root._autocompleteRoot = createRoot(root);
}
root._autocompleteRoot.render(<>{children}</>);
},
});
For new projects, use Preact via htm (the default) — smaller bundle, less ceremony. Save React-render integration for when the team needs it.
Pattern: a Client Component header that mounts Autocomplete on first render, in a portal-friendly container.
// components/header-search.tsx
'use client';
import { useEffect, useRef } from 'react';
export function HeaderSearch() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
let cleanup: (() => void) | undefined;
import('./autocomplete-instance').then(({ mount }) => {
cleanup = mount(containerRef.current!);
});
return () => cleanup?.();
}, []);
return <div ref={containerRef} className="w-full max-w-md" />;
}
// components/autocomplete-instance.ts
import { autocomplete } from '@algolia/autocomplete-js';
// ... configure plugins, sources
export function mount(container: HTMLElement) {
const instance = autocomplete({
container,
placeholder: 'Search…',
plugins: [/* ... */],
getSources: /* ... */,
});
return () => instance.destroy();
}
Dynamic import keeps Autocomplete out of the initial bundle. The header renders instantly; the dropdown wires up on hydration.
When the user presses Enter or clicks a query suggestion, route to the SERP with the query in the URL:
autocomplete({
// ...
navigator: {
navigate({ itemUrl }) {
window.location.assign(itemUrl);
},
navigateNewTab({ itemUrl }) {
window.open(itemUrl, '_blank');
},
},
onSubmit({ state }) {
window.location.assign(`/search?query=${encodeURIComponent(state.query)}`);
},
});
For client-side routing in Next.js, use useRouter instead of window.location. Wrap the navigator + onSubmit in a closure that has access to the router.
import { createAlgoliaInsightsPlugin } from '@algolia/autocomplete-plugin-algolia-insights';
import insightsClient from 'search-insights';
insightsClient('init', { appId: APP_ID, apiKey: SEARCH_KEY });
const insightsPlugin = createAlgoliaInsightsPlugin({ insightsClient });
autocomplete({
// ...
plugins: [insightsPlugin, recentSearchesPlugin, querySuggestionsPlugin],
});
The plugin sends view and click events with queryID automatically when items have __autocomplete_queryID (they do, when fetched via getAlgoliaResults).
The library handles:
aria-expanded, aria-controls, aria-activedescendant on the input.role="listbox", role="option" on the panel.You're responsible for:
aria-label on the input.The library ships base CSS (@algolia/autocomplete-theme-classic); skip it. Style the rendered DOM with Tailwind classes via the templates. Match shadcn's bg-popover, text-popover-foreground, ring tokens for focus, and rounded-md for the panel. The wrapper, panel, source, header, list, item slots all accept classNames / class attributes.
@algolia/client-search.getItems runs on every keystroke; expensive sources need debouncing.hitsPerPage per source: 3–5. Anything more clutters the dropdown.openOnFocus: true for the header pattern (shows recent + suggestions on focus); false for command palettes (require typing).useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
// open the autocomplete
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
Render Autocomplete inside a Dialog (shadcn <Dialog>); wire ⌘K to open / close.
The pattern at the top of this skill. Default for editorial sites with multiple content types.
getItems() {
return getAlgoliaResults({
searchClient: client,
queries: [{
indexName: 'locations',
params: {
query,
aroundLatLng: `${lat},${lng}`,
aroundRadius: 50000,
hitsPerPage: 5,
},
}],
});
}
algoliasearch. Reinvents keyboard nav, recent searches, source composition, accessibility. The library exists for a reason.noResults template. Empty-on-typo is a worse UX than "no matches; try X."algolia-instantsearch-react.algolia-search-client.algolia-relevance-tuning.algolia-analytics-events.algolia-api-keys-security.../../references/algolia-foundations.md