Search component
An accessible, dependency-free web component — the search box at the top of this page is one.
<script type="module" src="/_rangefind/rangefind-search.js"></script>
<link rel="stylesheet" href="/_rangefind/rangefind-search.css">
<rangefind-search src="/rangefind/" placeholder="Search…" hotkey></rangefind-search>
Both files ship inside the npm package (rangefind/element and
rangefind/element.css); the plugins copy them into your
build output automatically. The stylesheet is opt-in — the component
renders semantic, unstyled light DOM you can style however you like.
Attributes
| Attribute | Default | Meaning |
|---|---|---|
src |
/rangefind/ |
index base URL |
placeholder |
Search |
input placeholder |
page-size |
10 | results per page |
debounce |
— | input debounce in ms |
min-length |
— | minimum query length before searching |
highlight |
on | mark matched terms in snippets (highlight="false" to disable) |
suggest |
— | show autocomplete suggestions while typing |
router |
off | update ?q= in the URL and restore on load |
open-on-focus |
off | open the panel when the input focuses |
hotkey |
off | focus the box with / or ⌘K |
label |
— | accessible label for the input |
empty-text, loading-text, error-text |
sensible | state messages |
Booleans accept bare attributes (hotkey) or explicit values
(highlight="false").
Accessibility and keyboard
The component renders a combobox pattern: role="combobox" input,
aria-expanded, an options listbox with aria-activedescendant, arrow-key
navigation, Enter to open the active result, Esc to
close. Status text (result counts, loading, errors) lives in a polite live
region.
Theming
The default stylesheet exposes custom properties — override them on
.rf-search and the component follows your palette in light and dark:
.rf-search {
--rf-accent: #0e6f63;
--rf-mark-bg: #ffc940; /* match highlight */
--rf-radius: 8px;
--rf-bg: var(--your-surface);
--rf-panel-bg: var(--your-surface);
}
Prefer utility classes? Skip the stylesheet and target the part hooks with
*-class attributes instead — input-class, panel-class, option-class,
option-title-class, option-snippet-class, mark-class, and friends map
straight onto Tailwind:
<rangefind-search src="/rangefind/"
input-class="w-full rounded-lg border px-3 py-2"
panel-class="rounded-xl border bg-white shadow-xl"
option-class="px-3 py-2 hover:bg-amber-50"
mark-class="bg-amber-300 rounded-sm">
</rangefind-search>
Semantic search in the component
For per-query behavior — like embedding the query for
hybrid semantic search — set an async
transform on the element's searchOptions. It receives the params the
component is about to send and returns the params to use:
const box = document.querySelector("rangefind-search");
box.searchOptions = {
transform: async params => {
if (params.q) params.vector = await embedQuery(params.q); // your model
return params;
}
};
The search boxes on this site do exactly this: pages are embedded at build
time through the Eleventy plugin's enrich hook, and the browser lazily
loads the same MiniLM model to embed queries — lexical results are instant,
and rankings upgrade to hybrid once the model is warm. Static search
params (extra filters, facets) can be set the same way via
searchOptions.search.
In static site generators
Every plugin renders this markup for you with a shortcode or component —
{% rangefindSearch %} in Eleventy,
<RangefindSearch /> in Astro, and the equivalents for Docusaurus and
MkDocs. See Plugins.
For fully custom UIs, skip the component and drive the query API directly — the component is a convenience, not a requirement.