A debounced, keyboard-navigable, accessible autocomplete dropdown backed by a hosted search API. Debounce, highlighting, and a11y included — you bring the styling.
Get an API key, then test it against the live SearchJet index right here — no IDE, no setup.
Enter your email and we'll inject a working key straight into the sandbox below. No credit card. 10,000 free searches/month.
const params = new URLSearchParams({ q: 'wireless headphones', limit: '5' });
// Search as you type → instant results, no SDK needed
const res = await fetch(`https://app.searchjetengine.com/api/v1/search?${params}`, {
headers: { Authorization: 'Bearer 8e371cadae7aefedd3c99e41835df7ad709a27128c2a2d0c10ac408b1124c5a7' }
});
console.log(await res.json()); // ← live JSON below{
// Run a query above to see the live
// JSON response from the SearchJet API.
// Typo-tolerant, sub-150ms.
}Try “wireless headphons” (typo included). Keyboard arrows work too.
Debouncing, request races, keyboard navigation, accessibility, empty states — that's a week of work before you even test. The copy-paste code below handles it all.
Firing a fetch on every keystroke hammers your API and renders stale results. A production autocomplete needs debouncing, request cancellation, and race-condition guards.
Arrow-up / arrow-down to move through suggestions, Enter to select, Escape to dismiss — building accessible combobox behavior from scratch is notoriously fiddly and a common a11y failure.
Without a server-side "no match" strategy, your users hit dead ends. A great autocomplete also suggests popular queries when there are no exact hits.
A complete component that calls the API straight from the browser, or a headless hook that goes through your own server route.
import { useState, useEffect } from 'react';
// No SDK to install — one HTTP request to the SearchJet REST API.
const API = 'https://app.searchjetengine.com/api/v1/search';
const SEARCH_KEY = ''; // search key only — never the private key
// Wrap the first match of the query in <mark>
function Highlight({ text = '', query }) {
const q = query.trim().toLowerCase();
const i = q ? text.toLowerCase().indexOf(q) : -1;
if (i === -1) return text;
return <>{text.slice(0, i)}<mark>{text.slice(i, i + q.length)}</mark>{text.slice(i + q.length)}</>;
}
export default function Autocomplete() {
const [query, setQuery] = useState('');
const [hits, setHits] = useState([]);
const [active, setActive] = useState(-1);
useEffect(() => {
const q = query.trim();
if (!q) { setHits([]); return; }
const controller = new AbortController();
const timer = setTimeout(async () => {
try {
const res = await fetch(`${API}?q=${encodeURIComponent(q)}&limit=6`, {
headers: { Authorization: `Bearer ${SEARCH_KEY}` },
signal: controller.signal,
});
const data = await res.json();
setHits(data.results ?? []); // `results` is the array of hits
setActive(-1);
} catch (err) {
if (err.name !== 'AbortError') console.error(err);
}
}, 150); // debounce
return () => { clearTimeout(timer); controller.abort(); }; // cancel stale requests
}, [query]);
const go = (hit) => { window.location.href = hit.url; };
const onKeyDown = (e) => {
if (!hits.length) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => (a + 1) % hits.length); }
if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => (a <= 0 ? hits.length - 1 : a - 1)); }
if (e.key === 'Enter' && active > -1) go(hits[active]);
if (e.key === 'Escape') setHits([]);
};
return (
<div className="autocomplete">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown}
role="combobox"
aria-expanded={hits.length > 0}
aria-controls="suggestions"
aria-activedescendant={active > -1 ? `suggestion-${active}` : undefined}
placeholder="Search docs, products…"
/>
{hits.length > 0 && (
<ul id="suggestions" role="listbox">
{hits.map((hit, i) => (
<li
key={hit.id}
id={`suggestion-${i}`}
role="option"
aria-selected={i === active}
onMouseDown={() => go(hit)}
>
<Highlight text={hit.title} query={query} />
</li>
))}
</ul>
)}
</div>
);
}import { useState, useEffect } from 'react';
/**
* Headless autocomplete — full control, zero assumptions.
* Calls YOUR /api/search route (see the Next.js headless guide),
* which forwards to GET /search with the key from process.env.
*/
function useSuggestions(value) {
const [suggestions, setSuggestions] = useState([]);
const [status, setStatus] = useState('idle');
useEffect(() => {
const q = value.trim();
if (!q) {
setSuggestions([]);
setStatus('idle');
return;
}
const controller = new AbortController(); // race-condition guard
const timer = setTimeout(async () => {
setStatus('loading');
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
const data = await res.json();
setSuggestions(data.results ?? []); // array of hits
setStatus('success');
} catch (err) {
if (err.name !== 'AbortError') setStatus('error');
}
}, 150); // debounce
return () => {
controller.abort(); // drop stale responses
clearTimeout(timer);
};
}, [value]);
return { suggestions, status };
}
export function AutocompleteHeadless() {
const [query, setQuery] = useState('');
const [active, setActive] = useState(-1);
const { suggestions } = useSuggestions(query);
const onKeyDown = (e) => {
if (!suggestions.length) return;
if (e.key === 'ArrowDown') setActive((a) => (a + 1) % suggestions.length);
if (e.key === 'ArrowUp') setActive((a) => (a <= 0 ? suggestions.length - 1 : a - 1));
if (e.key === 'Enter' && active > -1) {
window.location.href = suggestions[active].url;
}
};
return (
<input
value={query}
onChange={(e) => { setQuery(e.target.value); setActive(-1); }}
onKeyDown={onKeyDown}
role="combobox"
aria-expanded={suggestions.length > 0}
placeholder="Type to search…"
/>
);
}Generate a key right on this page — the widget injects it straight into your clipboard-ready code.
There is no SDK — the component below calls GET /search with fetch. Copy it into your project as-is.
Debounced at 150ms, stale requests cancelled with AbortController, and Enter or click navigates to the hit URL. Change the limit, placeholder, or navigation to suit your app.
Style it with your own CSS. Keyboard navigation and combobox ARIA roles are already in the code. Done in minutes.
Free forever plan. 10,000 searches/month. No credit card.