> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Search Widget

> Add a search box with instant results to any website with one script tag.

> **Note**
>
> Product Search is in beta with design partners. Endpoints roll out per tenant.

`<captain-search>` is a search box for one Product Search index. Results update on every keystroke, matched words are highlighted, and the arrow keys, Enter and Escape work as expected. It runs on any page: plain HTML, React, Next.js, Vue, or a Shopify theme. The widget has no dependencies.

For a full search page (facets, sort, pagination), the same file family adds elements that share one search inside `<captain-search-root>`. See [Search pages](#search-pages).

The widget calls the same REST API documented in [Search](/guides/product-search/search) and [Analytics](/guides/product-search/analytics). No SDK is involved.

## Add it to a page

Load the script once, then place the element where the search box belongs.

```html
<script type="module"
  src="https://cdn.jsdelivr.net/gh/runcaptain/captain-search@1.7.3/captain-search.js"
  integrity="sha384-er2Nbxzoh1k6jdNJk6gYd2V1TPm3K26BJ7zT1WUtWL+tKoI9sxnrXVc2qwXyczLE"
  crossorigin="anonymous"></script>

<captain-search
  endpoint="https://acme.captain.dev"
  index="products"
  key="YOUR_SCOPED_KEY"
  placeholder="Search products"
></captain-search>
```

`@1.7.3` pins this exact release, and the `integrity` hash makes the browser refuse any other bytes. Each release lists its hashes in `integrity.json` in the [release repository](https://github.com/runcaptain/captain-search). To always get the newest 1.x release instead, load `@1` and leave out `integrity` and `crossorigin`: a moving version cannot carry a hash, and a new release can change the default markup.

The box loads recent searches, suggestion, completion and category rows, prices, badges, built image URLs and button render functions from a second file, `captain-search-extras.js`. It fetches the file only when the box needs it: on first focus for the row attributes, as soon as the page loads for `price-field`, `image-template` and `badge-field` (so the first results already show prices), and when a page sets a button render function with `el.templates`. It fetches it from the same exact release on jsDelivr and checks it against a hash built into the box, so the snippet stays one script tag. If your page has a Content Security Policy, allow `https://cdn.jsdelivr.net` in `script-src`.

Operators can build this snippet per index in the Product Search console: the tenant's **Integrate** tab, **Embed search**, mints a scoped key locked to your sites and fills it in, for the search box or a full search page.

| File                       | Contains                                                            | Size, gzipped |
| -------------------------- | ------------------------------------------------------------------- | ------------- |
| `captain-search.js`        | The search box and `createSearch`                                   | about 13 KB   |
| `captain-search-extras.js` | Loaded by the box when needed (see above)                           | about 5 KB    |
| `captain-instantsearch.js` | The same, plus `<captain-search-root>` and the search page elements | about 32 KB   |

In React and Next.js, render the element as JSX and load the script once, for example with `next/script`. The attributes are plain strings.

## Get a browser key

The key in the page is visible to anyone who views its source, so it must be a **scoped key** limited to your site. Do not put the search key or the write key in a page. Besides searching, the search key reads the index's analytics, which include what your users search for.

A scoped key can search and send events, but it cannot read analytics. Create one on your server with a write key for every index:

```bash
curl -X POST "https://acme.captain.dev/v1/keys" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "scoped",
  "indexes": ["products"],
  "filter": {"status": "published"},
  "allowed_origins": ["https://www.example.com", "https://example.com"],
  "description": "website search box"
}'
```

The response holds the key once. Copy it into the page.

* **`allowed_origins`** lists the sites that may use the key. A browser request from any other site is refused with `403 ORIGIN_NOT_ALLOWED`. List every origin the page is served from, including staging. Origins only bind browsers: a server or script can still send the key, so treat it as public.
* **`filter`** is optional. When set, it is applied to every search, and a request can narrow it but never widen it. Use it to say which records the page may show, for example only published ones. Without it, the key reads every record of its indexes.
* **`expires_at`** is optional. Without it the key lasts until it is revoked. To rotate a key, create the new one, deploy the page, then revoke the old one with `DELETE /v1/keys/{key_id}`.

When records carry access lists (see [Per-record access control](/guides/product-search/search#per-record-access-control)), mint a short-lived scoped key per signed-in user on your server, with the issuer key, and set it from script instead of an attribute:

```js
document.querySelector('captain-search').key = keyFromYourServer;
```

## Attributes

| Attribute                | Default                            | Purpose                                                                                                                                                                                                                                           |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint`               | required                           | The tenant URL from onboarding.                                                                                                                                                                                                                   |
| `index`                  | required                           | The index to search.                                                                                                                                                                                                                              |
| `key`                    | required                           | A scoped key. Can also be set with `el.key`.                                                                                                                                                                                                      |
| `placeholder`            | `Search`                           | Placeholder text. Also the accessible name unless `label` is set.                                                                                                                                                                                 |
| `label`                  | none                               | Accessible name for screen readers.                                                                                                                                                                                                               |
| `mode`                   | `dropdown`                         | `page` shows results under the box instead of in a dropdown, runs a full search on Enter, and pages with a More results button.                                                                                                                   |
| `k`                      | 8, or 20 in `page` mode            | Results per request, up to 50.                                                                                                                                                                                                                    |
| `debounce`               | 60                                 | Milliseconds to wait after a keystroke before searching. Fast typing and pastes send one request.                                                                                                                                                 |
| `min-chars`              | 1                                  | Characters typed before the first search.                                                                                                                                                                                                         |
| `filters`                | none                               | Filters for every search, as JSON: `{"category": "shoes"}`. Same syntax as the `filters` search parameter. Can also be set with `el.filters`.                                                                                                     |
| `title-field`            | `title` or `name`                  | Field shown as the result title.                                                                                                                                                                                                                  |
| `body-field`             | `description`, `text` or `content` | Field shown under the title, clipped to two lines.                                                                                                                                                                                                |
| `meta-fields`            | none                               | Comma-separated fields shown in small text, for example `brand,price`.                                                                                                                                                                            |
| `image-field`            | `image_url`                        | Field holding a thumbnail URL.                                                                                                                                                                                                                    |
| `image-template`         | none                               | Builds the thumbnail URL from fields, for example `https://img.shop.example/{image_id}/500x500.jpg`. Values are URL-encoded. Falls back to `image-field` when a field in it is empty or the image fails to load.                                  |
| `price-field`            | none                               | Field holding the price. Shows a price line under the title.                                                                                                                                                                                      |
| `compare-at-field`       | none                               | Field holding the old price. It shows struck through, with the saving, only when it's higher than the price.                                                                                                                                      |
| `currency`               | none                               | ISO currency code, for example `USD`. Prices are formatted with `Intl.NumberFormat`. Without it, prices show as plain numbers.                                                                                                                    |
| `locale`                 | the browser's                      | Locale for prices, for example `en-US` or `de-DE`.                                                                                                                                                                                                |
| `discount`               | `percent`                          | How the saving shows: `percent` (`40% off`), `amount` (`$80.00 off`) or `none`.                                                                                                                                                                   |
| `fraction-digits`        | the currency's                     | `auto` drops the cents on a whole price (`$400`, but `$399.50`), `0` rounds to whole units, `2` always shows two.                                                                                                                                 |
| `badge-field`            | none                               | Field shown as a badge above the result title, for example `tag`.                                                                                                                                                                                 |
| `sale-badge`             | none                               | Badge text for a result whose old price is higher, for example `Price Drop`. A `badge-field` value wins.                                                                                                                                          |
| `completions`            | 0                                  | Up to this many rows that complete the word being typed, above the results.                                                                                                                                                                       |
| `category-field`         | none                               | Field to show category rows for, above the results: its top values for what's typed, counted on the same request.                                                                                                                                 |
| `categories`             | 3                                  | How many category rows.                                                                                                                                                                                                                           |
| `category-url`           | none                               | Where a picked category goes, for example `/c/{value}?q={query}`. Values are URL-encoded.                                                                                                                                                         |
| `url-field`              | `url`                              | Field holding the link a result opens.                                                                                                                                                                                                            |
| `href-template`          | none                               | Builds the link from fields instead, for example `https://shop.example.com/p/{id}`. Values are URL-encoded.                                                                                                                                       |
| `highlight-fields`       | engine default                     | Comma-separated fields to highlight.                                                                                                                                                                                                              |
| `results-url`            | none                               | Where Enter goes, for example `/search?q={query}`. Without it, Enter runs a full search in place.                                                                                                                                                 |
| `query-param`            | none                               | On a results page, the URL parameter to read the starting query from, for example `q`.                                                                                                                                                            |
| `theme`                  | `light`                            | `dark`, or `auto` to follow the visitor's system setting.                                                                                                                                                                                         |
| `warm`                   | on                                 | Gets the first search ready before the first keystroke. See [Behavior under load](#behavior-under-load). `warm="false"` turns it off.                                                                                                             |
| `show-submit`            | off                                | Shows a search button. Without it the magnifier is decoration.                                                                                                                                                                                    |
| `show-reset`             | on                                 | Shows a clear button while the box has text. `show-reset="false"` hides it.                                                                                                                                                                       |
| `show-loading-indicator` | on                                 | Shows a spinner once a search takes longer than `stall-delay` (200 ms).                                                                                                                                                                           |
| `search-as-you-type`     | on                                 | `false` searches only on Enter or the search button.                                                                                                                                                                                              |
| `ignore-composition`     | off                                | Waits for IME input (Japanese, Chinese, Korean) to finish before searching.                                                                                                                                                                       |
| `autofocus`              | off                                | Focuses the box when the page loads.                                                                                                                                                                                                              |
| `render`                 | shadow                             | `light` renders into the page instead of a shadow root, so page CSS and `css-classes` style it.                                                                                                                                                   |
| `css-classes`            | none                               | Extra classes, as JSON: `{"input": "my-input", "submit": ["btn", "btn-primary"]}`. Keys: `root`, `form`, `input`, `submit`, `submitIcon`, `reset`, `resetIcon`, `loadingIndicator`, `loadingIcon`, `panel`, `list`, `item`. Also `el.cssClasses`. |
| `analytics`              | on                                 | `off` records nothing: no user token, no click events, and searches stay out of the search log.                                                                                                                                                   |
| `user-token`             | generated                          | The signed-in user's ID for analytics. Without it the widget generates a random token and keeps it in the browser.                                                                                                                                |

Only `http` and `https` links and images are used. Any other URL in a record is ignored.

To send requests through your own code, for example to add a header or go through a proxy, set `el.fetch`. It takes the same arguments as `fetch` and must return a `Response`. On a search page, set it on `<captain-search-root>`, whose search serves every element inside it:

```js
const box = document.querySelector('captain-search');
box.fetch = (url, init) => fetch(url, { ...init, headers: { ...init.headers, 'X-Store': 'eu' } });
```

### Product dropdown

Completion and category rows sit above the products, each group under its own heading. The products show a price, the old price struck through when it's higher, and the saving:

```html
<captain-search
  endpoint="https://acme.captain.dev"
  index="products"
  key="YOUR_SCOPED_KEY"
  href-template="https://shop.example.com/p/{id}"
  image-template="https://img.shop.example.com/{image_id}/500x500.jpg"
  price-field="price"
  compare-at-field="original_price"
  currency="USD"
  completions="3"
  category-field="category"
  category-url="https://shop.example.com/c/{value}?q={query}"
></captain-search>
```

Picking a completion searches for it. Picking a category fires `captain-search:category`, then opens `category-url`. Without `category-url`, handle the event yourself. Every row is reachable with the arrow keys.

## Results page

A dropdown in the site header can send Enter to a results page, and the results page can show the full list:

```html
<!-- header, on every page -->
<captain-search endpoint="https://acme.captain.dev" index="products" key="YOUR_SCOPED_KEY"
  results-url="/search?q={query}"></captain-search>

<!-- /search -->
<captain-search endpoint="https://acme.captain.dev" index="products" key="YOUR_SCOPED_KEY"
  mode="page" query-param="q"></captain-search>
```

## Styling

The widget looks finished without any CSS. To match a site, set CSS variables on the element or any parent:

```css
captain-search {
  --cs-accent: #ff6600;
  --cs-radius: 6px;
  --cs-font: "Inter", sans-serif;
}
```

| Variable                                           | Controls                                       |
| -------------------------------------------------- | ---------------------------------------------- |
| `--cs-accent`                                      | Focus ring, selected result, highlight tint    |
| `--cs-bg`, `--cs-fg`, `--cs-muted`, `--cs-border`  | Background, text, secondary text, borders      |
| `--cs-active-bg`                                   | Background of the selected result              |
| `--cs-radius`, `--cs-shadow`                       | Corner radius, dropdown shadow                 |
| `--cs-font`, `--cs-font-size`, `--cs-input-height` | Typography and box height                      |
| `--cs-width`, `--cs-max-height`, `--cs-z`          | Maximum width, dropdown height, stacking order |

For anything else, style the widget's parts with `::part()`: `root`, `form`, `input`, `submit`, `reset`, `loading-indicator`, `panel`, `listbox`, `option`, `link`, `image`, `title`, `body`, `meta`, `mark`, `message`, `more` and `footer`. With `render="light"` the widget has no shadow root: style its `cs-SearchBox-*` classes, or add classes with `css-classes`.

Templates replace the buttons and the spinner: `<template slot="submit">`, `<template slot="reset">` and `<template slot="loading">`. Render functions work too, for example ``box.templates = { submit: (data, { html }) => html`<b>Go</b>` }``. On a page with only `captain-search.js`, a render function loads `captain-search-extras.js` first, so that button draws a moment after the others. A `<template slot>` draws at once.

To change or skip a query before it is sent, set `queryHook`:

```js
document.querySelector('captain-search').queryHook = (query, search) => {
  if (query.length >= 3 || !query) search(query);
};
```

```css
captain-search::part(mark) { background: none; text-decoration: underline; }
```

### Custom result layout

Put a `<template>` inside the element to replace the layout of each result. `data-field` fills an element with a field's text, highlighted when it matched. `data-src` on an `img` sets its source from a field. With `price-field` set, an element with `data-price` gets the price line.

To lay out prices yourself, bind the values Captain computes for each result. They sit under `_captain` and need `price-field`, `image-template` or `badge-field` on the box:

| Field                 | Holds                                                    |
| --------------------- | -------------------------------------------------------- |
| `_captain.price`      | The formatted price                                      |
| `_captain.compare_at` | The formatted old price, only when it's higher           |
| `_captain.discount`   | The saving, for example `40% off`, only when on sale     |
| `_captain.on_sale`    | `true` when the old price is higher                      |
| `_captain.badge`      | The `badge-field` value, else `sale-badge` when on sale  |
| `_captain.image`      | The thumbnail from `image-template`, else `image-field`  |
| `_captain.url`        | The result's link from `href-template`, else `url-field` |

```html
<template>
  <img data-src="_captain.image" width="48" height="48" alt="">
  <strong data-field="title"></strong>
  <span data-field="_captain.price"></span> <s data-field="_captain.compare_at"></s>
</template>
```

```html
<captain-search endpoint="https://acme.captain.dev" index="products" key="YOUR_SCOPED_KEY">
  <template>
    <img data-src="image_url" width="48" height="48" alt="">
    <div>
      <strong data-field="title"></strong>
      <span data-field="brand"></span> · $<span data-field="price"></span>
    </div>
  </template>
</captain-search>
```

Record values are always inserted as text, never as HTML, so a record cannot inject markup or script into the page.

## Analytics

The widget sends `user_token` with every search and a click event when a result is opened, with the search's `query_id` and the result's position. Click-through rate, no-click searches and top hits in [Analytics](/guides/product-search/analytics) work without extra code. Set `user-token` to the signed-in user's ID to tie searches across devices.

Set `analytics="off"` when a visitor opts out. The widget then sends no user token and no click events, and sends every search with `"analytics": false`, so it isn't logged or counted in Analytics. An index on an engine older than RC11.9.1 refuses that field once. The widget then searches without it, still with no user token, so those searches are counted without being tied to the visitor.

## Events

The element dispatches DOM events for pages that need to react:

| Event                     | `detail`                                      | Cancel it to                                                      |
| ------------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
| `captain-search:query`    | `query`                                       |                                                                   |
| `captain-search:results`  | `query`, `hits`, `total`, `queryId`, `facets` |                                                                   |
| `captain-search:select`   | `hit`, `position`, `queryId`, `href`          | Handle navigation yourself, for example with a client-side router |
| `captain-search:submit`   | `query`                                       | Handle Enter yourself                                             |
| `captain-search:category` | `field`, `value`, `query`, `href`             | Handle a picked category row yourself                             |
| `captain-search:error`    | `error`                                       |                                                                   |

```js
document.querySelector('captain-search').addEventListener('captain-search:select', (e) => {
  e.preventDefault();
  router.push(`/products/${e.detail.hit.id}`);
});
```

The click event is sent before `captain-search:select` fires, so cancelling navigation still records the click.

## Keyboard and accessibility

The widget follows the ARIA combobox pattern. Up and Down move through results, Home and End jump to the first and last, Enter opens the selected result, and Escape closes the list (a second Escape clears the box). Focus stays in the input, screen readers hear the selected result and the result count, and mouse users can open a result in a new tab as with any link.

## Behavior under load

Each keystroke cancels the request still in flight, and an answer that arrives late never replaces a newer one. The previous results stay on screen until new ones arrive, so the list does not flicker. When the tenant answers `429` or `503`, the widget waits for `Retry-After` and retries once before showing an error.

The first search of a visit is the slowest: the browser opens a connection and checks that your page may call the tenant. The widget does that work early. When the page loads, it tells the browser to connect to your tenant. When the shopper first focuses the box, or when a search page loads, it sends one small request that the edge answers on its own, so no search runs and nothing is counted in Analytics. It sends it at most once per page. In our tests from a fresh browser, the first answer arrived about 100 ms sooner. To turn it off, set `warm="false"` on the box or on `<captain-search-root>`. A page that sends requests through its own `el.fetch` is left alone.

## Search pages

Load `captain-instantsearch.js` and put the elements inside one `<captain-search-root>`. They share one search: typing in the box, ticking a facet or changing the sort updates every element.

```html
<script type="module"
  src="https://cdn.jsdelivr.net/gh/runcaptain/captain-search@1.7.3/captain-instantsearch.js"
  integrity="sha384-ffOTefDQQjKDOi6udKCEnrcDhpxiwXdHQyNXwZrqof1DPs8/jxUGNzV7mRbLU3rx"
  crossorigin="anonymous"></script>

<captain-search-root endpoint="https://acme.captain.dev" index="products" key="YOUR_SCOPED_KEY" hits-per-page="20" url-sync>
  <captain-search placeholder="Search products"></captain-search>
  <captain-stats></captain-stats>
  <captain-sort-by>
    <option value="">Most relevant</option>
    <option value="price:asc">Price, low to high</option>
  </captain-sort-by>
  <captain-refinement-list attribute="brand" label="Brand" searchable show-more></captain-refinement-list>
  <captain-range-input attribute="price" label="Price" min="0" max="500"></captain-range-input>
  <captain-current-refinements></captain-current-refinements>
  <captain-clear-refinements></captain-clear-refinements>
  <captain-hits href-template="https://shop.example.com/p/{id}">
    <template slot="empty">Nothing matches <q data-query></q>.</template>
  </captain-hits>
  <captain-pagination></captain-pagination>
</captain-search-root>
```

| Element                       | Shows                                                                      | Attributes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `captain-search-root`         | Nothing. Holds the search.                                                 | `endpoint`, `index`, `key`, `filters`, `hits-per-page` (20), `debounce`, `stall-delay`, `search-on-empty` (on: results before anything is typed), `url-sync`, `url-prefix`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `captain-search`              | The input                                                                  | As above. Inside a root it shows no dropdown.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `captain-hits`                | Results                                                                    | `title-field`, `body-field`, `meta-fields`, `image-field`, `url-field`, `href-template`, and the box's price and image attributes: `price-field`, `compare-at-field`, `currency`, `locale`, `discount`, `image-template`, `badge-field`, `sale-badge`. `fraction-digits` works here too. A custom item template reads the computed values as `_captain.price`, `_captain.compare_at`, `_captain.discount`, `_captain.on_sale`, `_captain.badge`, `_captain.image` and `_captain.url`, for example `<s data-if="_captain.on_sale" data-field="_captain.compare_at">` or `<a data-attr-href="_captain.url">`. A `<template>` for the layout, named `slot="item"` or unnamed, (`data-field`, `data-snippet`, `img data-src`), `<template slot="empty">` for no results. |
| `captain-infinite-hits`       | Results with a Show more button                                            | As `captain-hits`, plus `show-more-label`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `captain-pagination`          | Page links                                                                 | `padding` (3), `show-first`, `show-previous`, `show-next`, `show-last`, `scroll-to`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `captain-stats`               | "1,234 results in 12 ms"                                                   | A `<template>` whose elements carry `data-stat`: `nbHits`, `ms`, `query`, `page` or `nbPages`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `captain-refinement-list`     | Facet values with counts                                                   | `attribute`, `operator` (`or` or `and`), `limit` (10), `show-more`, `show-more-limit` (20), `searchable`, `sort-by` (`count`, `name`, `name:desc`), `label`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `captain-sort-by`             | A sort menu                                                                | `<option>` children: `value="price:asc"` or `"price:desc"`, and `""` for relevance                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `captain-hits-per-page`       | A page-size menu                                                           | `<option value="20">` children, up to 50                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `captain-current-refinements` | Removable chips for each filter                                            | `labels` (JSON, field to label), `include-query`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `captain-clear-refinements`   | A clear button                                                             | `included-attributes`, `include-query`, `label`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `captain-range-input`         | Min and max inputs                                                         | `attribute`, `min`, `max`, `step`, `input-type="date"`, `label`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `captain-toggle`              | One checkbox filter                                                        | `attribute`, `value`, `label`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `captain-no-results`          | Its own content, only when nothing matches                                 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `captain-completions`         | Word completions while typing, or the spelling the engine searched instead | `limit`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

Every element takes `css-classes` and renders into the page with `cs-*` class names (`cs-Hits-item`, `cs-RefinementList-count`, and so on). The default look comes from one stylesheet whose rules have no specificity, so any page CSS overrides it. The same `--cs-*` variables apply.

### URL sync

With `url-sync` on the root, the query, page, filters, ranges, sort and page size live in the address bar, for example `?q=boots&brand=acme&price=50:200&sort=price:asc&page=2`. Typing replaces the current history entry, filtering and paging add one, and Back and Forward restore the search. Other parameters are left alone. `url-prefix` namespaces the parameters when one page has two searches.

### How the elements call the API

| When                                               | Route                                                                                                                |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Typing, with no facets, sort or paging on the page | `POST /v1/indexes/{index}/typeahead`                                                                                 |
| A query with facets, sort or paging                | `POST /v1/indexes/{index}/search`                                                                                    |
| No query yet (`search-on-empty`)                   | `POST /v1/indexes/{index}/browse`                                                                                    |
| A refined OR facet                                 | `POST /v1/multi_search`: the main search plus one per refined facet, so each facet's counts ignore its own selection |
| A searchable refinement list                       | `search` or `browse` with `facet_query`                                                                              |
| A click or conversion                              | `POST /v1/events`                                                                                                    |

### Limits

* **Facet counts cover the most relevant matches, not every match.** When a list's counts cover fewer records than the result count, the refinement list says so under the list ("Counts cover the top 53 of 268 matches").
* **Sort orders the top 1,000 relevant records** for a query. Pages stop at the 1,000th record, and a page holds at most 50.
* **Range bounds** come from `facet_stats` only for number fields that are facets. For other fields, set `min` and `max` on `captain-range-input`.
* **AND inside one facet** (`operator="and"`) works on indexes with typed fields, through the filter expression.
* **Counting facets with many distinct values over a whole large index is slow.** A browse that counts authors across 349,000 records took about 4 seconds. On such an index, set `search-on-empty="false"` or leave high-cardinality lists off the empty page.
* Not available: hierarchical menus and breadcrumbs, Query Suggestions and recent searches, dynamic facet ordering, a geo map, voice search, and AI answers.

## Headless

`createSearch` is the same search with no UI, for pages that draw their own:

```js
import { createSearch } from 'https://cdn.jsdelivr.net/gh/runcaptain/captain-search@1.7.3/captain-search.js';

const search = createSearch({ endpoint: 'https://acme.captain.dev', index: 'products', key: 'YOUR_SCOPED_KEY' });
input.addEventListener('input', () => search.refine(input.value));
search.on('results', (r) => {
  list.replaceChildren(...(r?.hits ?? []).map((hit) => {
    const li = document.createElement('li');
    li.textContent = hit.title; // record text as text, never as HTML
    return li;
  }));
});
```

| Member                                                                                                 | Does                                                                                       |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `query`, `state`, `results`, `isSearchStalled`                                                         | Read the current search                                                                    |
| `refine(query)`, `clear()`                                                                             | Set or clear the query                                                                     |
| `setPage(n)`, `setHitsPerPage(n)`, `setSort('field:asc')`                                              | Page, size and order                                                                       |
| `addFacet(field)`, `toggleRefinement(field, value)`, `setRange(field, min, max)`, `clearRefinements()` | Filters                                                                                    |
| `searchFacetValues(field, prefix)`                                                                     | Search inside one facet's values                                                           |
| `sendClick(hit, position)`, `sendConversion(hit, name)`                                                | Analytics events                                                                           |
| `on(event, fn)`                                                                                        | Subscribe to `query`, `results`, `state` or `error`. Returns a function that unsubscribes. |

`createSearch` also takes `fetch` (your own request function), `analytics: false` (the same opt-out as `analytics="off"`) and `suggestFacets` (fields to count on every search, typeahead included).

Inside a root, the same object is `document.querySelector('captain-search-root').controller`.