Try it live - type, then put the mouse away and use the arrow keys.
Instant client-side search from a prebuilt JSON index, extracted from The DJ Calendar, where it powers search across a site of ~1,000 pages with no search server, no framework, and no library.
Most search boxes are infrastructure cosplay: a request to a search endpoint, a spinner, a payload, for what is usually a list of a few hundred names that the page could have shipped with in the first place.
This repo’s stance: if the catalog fits in the page, the search belongs in the page. Ship the index as JSON, score it in the browser on every keystroke, and the search feels instant because it is - there is no network left to wait on. No server round-trips. No framework. No library. One file.
Declarative - the index travels in the attribute, the widget builds itself:
<link rel="stylesheet" href="src/search.css">
<script src="src/search.js" defer></script>
<input type="text" data-instant-search='[
{"title": "Carl Cox", "subtitle": "Techno", "keywords": "techno house icon", "badge": "Artist", "url": "/artists/carl-cox"},
{"title": "Amelie Lens", "subtitle": "Techno", "keywords": "techno belgium", "badge": "Artist", "url": "/artists/amelie-lens"}
]'>
Programmatic - for records that live in JS or arrive at runtime:
InstantSearch.attach(inputEl, records, { maxResults: 6, placeholder: "Find an artist..." });
One JSON array. Three searchable fields and two display-only ones:
{
"title": "Carl Cox",
"subtitle": "Techno / House",
"keywords": "techno house icon ibiza space",
"badge": "Artist",
"url": "/artists/carl-cox"
}
title - required. The primary match surface, weighted heaviest.subtitle - optional. Displayed right-aligned; lightly searchable.keywords - optional. The synonym field: genre names, city names, whatever
your users type that isn’t in the title.badge - optional. Display-only tag (“Artist”, “Venue”, “City”).url - where Enter navigates.The production trick behind this contract: the server renders the index once at page build time, so the client never fetches anything at search time. Search-time work is zero; the only payload is data the page wanted anyway.
Every keystroke scores all records; a record must match every query token somewhere (AND semantics) or it drops out. Where a match lands decides rank:
| Signal | Weight | Meaning |
|---|---|---|
| Title token exact | 60 | “cox” hits “Cox” |
| Title token prefix | 40 | “ame” hits “Amelie” |
| Keyword exact | 30 | “techno” hits the genre field |
| Keyword prefix | 18 | “tech” hits “techno” |
| Subtitle exact / prefix | 12 / 8 | useful, but not the point |
| Loose substring | 4 | the safety net |
| Full-phrase hit | +25 | “black coffee” beats token soup |
Ties break toward shorter titles - “Carl Cox” outranks “The Carl Cox Experience” - then toward index order, which keeps results stable.
Precomputed token lists are built once at load, so a keystroke scores arrays, never re-splits strings. On a few hundred records, this is microseconds.
The input is a real ARIA combobox: role="combobox", aria-expanded,
aria-controls, aria-activedescendant pointing into a role="listbox" of
options. The keys:
| Key | Does |
|---|---|
ArrowDown / ArrowUp |
move the active option |
Enter |
go to the active option; if exactly one result, go straight there |
Escape |
close the list |
Matched text is highlighted with <mark> in the results. Everything is
escaped before it is rendered; index entries are data, not markup.
| Option | Default | Meaning |
|---|---|---|
minLength |
2 |
queries shorter than this open nothing |
maxResults |
8 |
cap the list - the top of the list is the product |
debounce |
80 |
ms of typing silence before re-scoring |
emptyMessage |
"No matches." |
shown when nothing matches |
placeholder |
"Search..." |
input placeholder |
Extracted from The DJ Calendar (https://thedjcalendar.com), where this pattern has run site search across ~1,000 pages since 2015 without a search server. Like everything published under this account, it is a production-derived pattern: what ships here is the idea, not the infrastructure.
MIT