@headband/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZVN DEV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @headband/react
2
2
 
3
- React hooks and components for [Headband](https://headband.dev) search. Drop-in building blocks for building fast, faceted search UIs -- designed as an InstantSearch-compatible alternative that works with Headband's unified search API.
3
+ React hooks and components for [Headband](https://headband.dev) search. Drop-in building blocks for building fast, faceted search UIs -- designed as an InstantSearch-inspired alternative that works with Headband's unified search API.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,6 +10,8 @@ npm install @headband/react
10
10
 
11
11
  **Peer dependencies:** React 18+ and React DOM 18+.
12
12
 
13
+ Browser SDK examples must use scoped `hb_src_*` search keys. Keep `hb_adm_*` admin keys server-side for indexing, settings, and connector jobs.
14
+
13
15
  ## Quick Start
14
16
 
15
17
  ```tsx
@@ -23,7 +25,7 @@ import {
23
25
  Stats,
24
26
  } from "@headband/react";
25
27
 
26
- const client = headband("https://your-headband-instance.com", "your-api-key");
28
+ const client = headband("https://your-headband-instance.com", "hb_src_YOUR_SEARCH_KEY");
27
29
 
28
30
  function App() {
29
31
  return (
@@ -68,6 +70,33 @@ HeadbandProvider -- owns all search state, executes searches
68
70
  |-- useSearch() -- full state + actions (escape hatch)
69
71
  ```
70
72
 
73
+ ### Base filters and full results pages
74
+
75
+ Use `baseFilter` on `HeadbandProvider` for app-owned constraints that should apply
76
+ to every query: tenant isolation, RBAC, publish status, availability, or custom
77
+ OR/AND groups. Refinement and range hooks keep adding UI filters, and Headband
78
+ ANDs them with `baseFilter` while preserving OR groups.
79
+
80
+ ```tsx
81
+ import { HeadbandProvider, filterEq, orFilters } from "@headband/react";
82
+
83
+ const availability = orFilters(filterEq("status", "published"), filterEq("status", "backorder"));
84
+
85
+ <HeadbandProvider
86
+ client={client}
87
+ index="products"
88
+ baseFilter={[filterEq("tenantId", "acme"), availability]}
89
+ >
90
+ <SearchBox />
91
+ <RefinementList attribute="brand" />
92
+ <RangeInput attribute="price" />
93
+ <Hits />
94
+ </HeadbandProvider>;
95
+ ```
96
+
97
+ This supports text search plus filters, filter-only browse pages, and mixed
98
+ boolean filters on every Headband engine.
99
+
71
100
  ---
72
101
 
73
102
  ## Client
@@ -79,15 +108,62 @@ Creates a Headband API client.
79
108
  ```ts
80
109
  import { headband } from "@headband/react";
81
110
 
82
- const client = headband("https://search.example.com", "your-api-key");
111
+ const client = headband("https://search.example.com", "hb_src_YOUR_SEARCH_KEY");
83
112
  ```
84
113
 
85
- | Param | Type | Description |
86
- | -------- | -------- | --------------------------------- |
87
- | `host` | `string` | Headband instance URL |
88
- | `apiKey` | `string` | API key for authentication |
114
+ | Param | Type | Description |
115
+ | -------- | -------- | -------------------------- |
116
+ | `host` | `string` | Headband instance URL |
117
+ | `apiKey` | `string` | API key for authentication |
118
+
119
+ Returns a `HeadbandClient` with `search()`, `multiSearch()`, `getExperimentConfigs()`, `assignExperiment()`, and `recordExperimentEvent()` methods.
120
+
121
+ ---
122
+
123
+ ## Retrieval experiments
89
124
 
90
- Returns a `HeadbandClient` with `search()` and `multiSearch()` methods.
125
+ Headband can store managed experiment rules, rollout percentages, variant weights, and kill-switch state. Apps can call hosted `client.assignExperiment()` when they want Headband to apply those rules, or use deterministic local helpers when offline/app-owned assignment is required.
126
+
127
+ ```tsx
128
+ import { HeadbandProvider, headband } from "@headband/react";
129
+
130
+ const client = headband("https://headband.dev", "hb_src_your_search_key");
131
+ const assignment = await client.assignExperiment({
132
+ indexUid: "products",
133
+ experimentKey: "engine-ab-test",
134
+ subjectId: sessionId,
135
+ sessionId,
136
+ });
137
+
138
+ if (assignment.assigned) {
139
+ await client.recordExperimentEvent({
140
+ eventType: "experiment_view",
141
+ experimentKey: assignment.experimentKey,
142
+ variant: assignment.variant!,
143
+ indexUid: assignment.indexUid!,
144
+ sessionId,
145
+ });
146
+ }
147
+
148
+ <HeadbandProvider
149
+ client={client}
150
+ index={assignment.assigned ? assignment.indexUid! : "products"}
151
+ experiment={
152
+ assignment.assigned
153
+ ? {
154
+ experimentKey: assignment.experimentKey,
155
+ variant: assignment.variant!,
156
+ sessionId: assignment.sessionId,
157
+ }
158
+ : undefined
159
+ }
160
+ >
161
+ <SearchBox />
162
+ <Hits />
163
+ </HeadbandProvider>;
164
+ ```
165
+
166
+ Hosted assignment only returns a variant when the rule is `active`, the kill switch is off, and the subject falls inside `trafficPercent`. For local assignment, `assignExperiment(config, subjectId)` uses `assignmentSalt`, `experimentKey`, and your subject/session ID for stable bucketing. Headband receives `experimentKey`, `variant`, and `sessionId` on search calls for analytics attribution.
91
167
 
92
168
  ---
93
169
 
@@ -98,21 +174,23 @@ Returns a `HeadbandClient` with `search()` and `multiSearch()` methods.
98
174
  Wraps your search UI and manages all search state.
99
175
 
100
176
  ```tsx
101
- <HeadbandProvider
102
- client={client}
103
- index="products"
104
- initialQuery="shoes"
105
- >
177
+ <HeadbandProvider client={client} index="products" initialQuery="shoes">
106
178
  {/* search components */}
107
179
  </HeadbandProvider>
108
180
  ```
109
181
 
110
- | Prop | Type | Default | Description |
111
- | -------------- | ----------------- | ------- | ---------------------------------------- |
112
- | `client` | `HeadbandClient` | -- | Client returned by `headband()` |
113
- | `index` | `string` | -- | Index to search |
114
- | `initialQuery` | `string` | `""` | Starting query value |
115
- | `children` | `ReactNode` | -- | Child components |
182
+ | Prop | Type | Default | Description |
183
+ | -------------- | ---------------------------------- | ------- | ------------------------------------------------------ |
184
+ | `client` | `HeadbandClient` | -- | Client returned by `headband()` |
185
+ | `index` | `string` | -- | Index to search |
186
+ | `initialQuery` | `string` | `""` | Starting query value |
187
+ | `experiment` | `ExperimentSearchMetadata \| null` | `null` | Optional experiment telemetry attached to search calls |
188
+ | `mode` | `"instant" \| "retrieval"` | -- | Visor's ranking mode; other engines ignore it |
189
+ | `children` | `ReactNode` | -- | Child components |
190
+
191
+ `mode` is also a field of `client.search()` params and of each `client.multiSearch()` query.
192
+ On a Visor index, `instant` (the default) is typo-tolerant, as-you-type search, and
193
+ `retrieval` ranks longer queries by BM25F.
116
194
 
117
195
  ---
118
196
 
@@ -126,11 +204,11 @@ Binds to the search input. Returns the current query plus setters.
126
204
  const { query, setQuery, clear } = useSearchBox();
127
205
  ```
128
206
 
129
- | Return | Type | Description |
130
- | ---------- | -------------------------- | ---------------------------------------------- |
131
- | `query` | `string` | Current query string |
132
- | `setQuery` | `(query: string) => void` | Update the query (resets pagination) |
133
- | `clear` | `() => void` | Clear the query to an empty string |
207
+ | Return | Type | Description |
208
+ | ---------- | ------------------------- | ------------------------------------ |
209
+ | `query` | `string` | Current query string |
210
+ | `setQuery` | `(query: string) => void` | Update the query (resets pagination) |
211
+ | `clear` | `() => void` | Clear the query to an empty string |
134
212
 
135
213
  ---
136
214
 
@@ -142,11 +220,11 @@ Returns the current search result hits with loading and error state.
142
220
  const { hits, isLoading, error } = useHits<Movie>();
143
221
  ```
144
222
 
145
- | Return | Type | Description |
146
- | ----------- | ------------- | ---------------------------------------------- |
147
- | `hits` | `Hit<T>[]` | Array of hit objects |
148
- | `isLoading` | `boolean` | Whether a search is in progress |
149
- | `error` | `Error\|null` | Error from the last search, if any |
223
+ | Return | Type | Description |
224
+ | ----------- | ------------- | ---------------------------------- |
225
+ | `hits` | `Hit<T>[]` | Array of hit objects |
226
+ | `isLoading` | `boolean` | Whether a search is in progress |
227
+ | `error` | `Error\|null` | Error from the last search, if any |
150
228
 
151
229
  Each `Hit<T>` includes the original document fields, an optional `_formatted` object with highlighted values, and a typed `__data: T` accessor.
152
230
 
@@ -164,18 +242,18 @@ const { items, refine, canToggle, isLoading } = useRefinementList({
164
242
  });
165
243
  ```
166
244
 
167
- | Prop | Type | Default | Description |
168
- | ----------- | -------------------- | --------- | ------------------------------------ |
169
- | `attribute` | `string` | -- | Facet attribute (e.g. `"genres"`) |
170
- | `limit` | `number` | `20` | Max facet values to show |
171
- | `sortBy` | `"count" \| "alpha"` | `"count"` | Sort order for facet values |
245
+ | Prop | Type | Default | Description |
246
+ | ----------- | -------------------- | --------- | --------------------------------- |
247
+ | `attribute` | `string` | -- | Facet attribute (e.g. `"genres"`) |
248
+ | `limit` | `number` | `20` | Max facet values to show |
249
+ | `sortBy` | `"count" \| "alpha"` | `"count"` | Sort order for facet values |
172
250
 
173
- | Return | Type | Description |
174
- | ----------- | ------------------------ | ------------------------------------- |
175
- | `items` | `RefinementListItem[]` | Facet values with count & state |
176
- | `refine` | `(value: string) => void`| Toggle a facet value on/off |
177
- | `canToggle` | `boolean` | Whether any values are available |
178
- | `isLoading` | `boolean` | Loading state |
251
+ | Return | Type | Description |
252
+ | ----------- | ------------------------- | -------------------------------- |
253
+ | `items` | `RefinementListItem[]` | Facet values with count & state |
254
+ | `refine` | `(value: string) => void` | Toggle a facet value on/off |
255
+ | `canToggle` | `boolean` | Whether any values are available |
256
+ | `isLoading` | `boolean` | Loading state |
179
257
 
180
258
  Each `RefinementListItem` has `{ value: string; count: number; isRefined: boolean }`.
181
259
 
@@ -190,15 +268,15 @@ const { currentPage, totalPages, totalHits, setPage, canPrevious, canNext, pages
190
268
  usePagination();
191
269
  ```
192
270
 
193
- | Return | Type | Description |
194
- | ------------- | ------------------------- | ------------------------------------ |
195
- | `currentPage` | `number` | Current page index (0-based) |
196
- | `totalPages` | `number` | Total number of pages |
197
- | `totalHits` | `number` | Total result count |
198
- | `setPage` | `(page: number) => void` | Navigate to a page (clamped) |
199
- | `canPrevious` | `boolean` | Whether a previous page exists |
200
- | `canNext` | `boolean` | Whether a next page exists |
201
- | `pages` | `number[]` | Window of page numbers for rendering |
271
+ | Return | Type | Description |
272
+ | ------------- | ------------------------ | ------------------------------------ |
273
+ | `currentPage` | `number` | Current page index (0-based) |
274
+ | `totalPages` | `number` | Total number of pages |
275
+ | `totalHits` | `number` | Total result count |
276
+ | `setPage` | `(page: number) => void` | Navigate to a page (clamped) |
277
+ | `canPrevious` | `boolean` | Whether a previous page exists |
278
+ | `canNext` | `boolean` | Whether a next page exists |
279
+ | `pages` | `number[]` | Window of page numbers for rendering |
202
280
 
203
281
  ---
204
282
 
@@ -210,12 +288,12 @@ Returns summary statistics about the current search.
210
288
  const { totalHits, processingTimeMs, query, isLoading } = useStats();
211
289
  ```
212
290
 
213
- | Return | Type | Description |
214
- | ----------------- | --------- | -------------------------------- |
215
- | `totalHits` | `number` | Total results for the query |
216
- | `processingTimeMs`| `number` | Server-side processing time (ms) |
217
- | `query` | `string` | The query that produced results |
218
- | `isLoading` | `boolean` | Loading state |
291
+ | Return | Type | Description |
292
+ | ------------------ | --------- | -------------------------------- |
293
+ | `totalHits` | `number` | Total results for the query |
294
+ | `processingTimeMs` | `number` | Server-side processing time (ms) |
295
+ | `query` | `string` | The query that produced results |
296
+ | `isLoading` | `boolean` | Loading state |
219
297
 
220
298
  ---
221
299
 
@@ -233,17 +311,17 @@ const { currentSort, setSort, items } = useSortBy({
233
311
  });
234
312
  ```
235
313
 
236
- | Prop | Type | Description |
237
- | ------- | -------------- | ----------------------------------------- |
238
- | `items` | `SortByItem[]` | Available sort options |
314
+ | Prop | Type | Description |
315
+ | ------- | -------------- | ---------------------- |
316
+ | `items` | `SortByItem[]` | Available sort options |
239
317
 
240
318
  Each `SortByItem` has `{ value: string; label: string }`.
241
319
 
242
- | Return | Type | Description |
243
- | ------------- | --------------------------------- | ------------------------------ |
320
+ | Return | Type | Description |
321
+ | ------------- | --------------------------------- | ------------------------------------ |
244
322
  | `currentSort` | `string \| null` | Active sort value (null = relevance) |
245
- | `setSort` | `(value: string\|null) => void` | Set the sort order |
246
- | `items` | `(SortByItem & { isSelected })[]` | Items with `isSelected` flag |
323
+ | `setSort` | `(value: string\|null) => void` | Set the sort order |
324
+ | `items` | `(SortByItem & { isSelected })[]` | Items with `isSelected` flag |
247
325
 
248
326
  ---
249
327
 
@@ -257,18 +335,18 @@ const { min, max, stats, setRange, clear, isLoading } = useRange({
257
335
  });
258
336
  ```
259
337
 
260
- | Prop | Type | Description |
261
- | ----------- | -------- | ------------------------------------- |
262
- | `attribute` | `string` | Numeric attribute (e.g. `"price"`) |
338
+ | Prop | Type | Description |
339
+ | ----------- | -------- | ---------------------------------- |
340
+ | `attribute` | `string` | Numeric attribute (e.g. `"price"`) |
263
341
 
264
- | Return | Type | Description |
265
- | ----------- | ------------------------------------------ | -------------------------------- |
266
- | `min` | `number \| undefined` | Current min bound |
267
- | `max` | `number \| undefined` | Current max bound |
268
- | `stats` | `{ min: number; max: number } \| undefined`| Overall min/max from engine |
269
- | `setRange` | `(range: { min?: number; max?: number }) => void` | Set range filter |
270
- | `clear` | `() => void` | Remove the range filter |
271
- | `isLoading` | `boolean` | Loading state |
342
+ | Return | Type | Description |
343
+ | ----------- | ------------------------------------------------- | --------------------------- |
344
+ | `min` | `number \| undefined` | Current min bound |
345
+ | `max` | `number \| undefined` | Current max bound |
346
+ | `stats` | `{ min: number; max: number } \| undefined` | Overall min/max from engine |
347
+ | `setRange` | `(range: { min?: number; max?: number }) => void` | Set range filter |
348
+ | `clear` | `() => void` | Remove the range filter |
349
+ | `isLoading` | `boolean` | Loading state |
272
350
 
273
351
  ---
274
352
 
@@ -283,15 +361,15 @@ const { value, highlighted } = useHighlight({
283
361
  });
284
362
  ```
285
363
 
286
- | Prop | Type | Description |
287
- | ----------- | ------------------------ | ---------------------------------- |
288
- | `hit` | `Record<string, unknown>`| The hit object from `useHits()` |
289
- | `attribute` | `string` | Attribute to highlight |
364
+ | Prop | Type | Description |
365
+ | ----------- | ------------------------- | ------------------------------- |
366
+ | `hit` | `Record<string, unknown>` | The hit object from `useHits()` |
367
+ | `attribute` | `string` | Attribute to highlight |
290
368
 
291
- | Return | Type | Description |
292
- | ------------- | --------- | ---------------------------------------------- |
293
- | `value` | `string` | Highlighted HTML string (safe for innerHTML) |
294
- | `highlighted` | `boolean` | Whether the value contains highlight marks |
369
+ | Return | Type | Description |
370
+ | ------------- | --------- | -------------------------------------------- |
371
+ | `value` | `string` | Highlighted HTML string (safe for innerHTML) |
372
+ | `highlighted` | `boolean` | Whether the value contains highlight marks |
295
373
 
296
374
  Supports nested attributes via dot notation (e.g. `"author.name"`).
297
375
 
@@ -303,9 +381,25 @@ Escape-hatch hook that exposes the full search state and all actions. Prefer the
303
381
 
304
382
  ```tsx
305
383
  const {
306
- query, results, isLoading, error, refinements, ranges, sortBy, page,
307
- hitsPerPage, facets, setQuery, setPage, setSortBy, setHitsPerPage,
308
- toggleRefinement, setRange, clearRange, registerFacet, unregisterFacet,
384
+ query,
385
+ results,
386
+ isLoading,
387
+ error,
388
+ refinements,
389
+ ranges,
390
+ sortBy,
391
+ page,
392
+ hitsPerPage,
393
+ facets,
394
+ setQuery,
395
+ setPage,
396
+ setSortBy,
397
+ setHitsPerPage,
398
+ toggleRefinement,
399
+ setRange,
400
+ clearRange,
401
+ registerFacet,
402
+ unregisterFacet,
309
403
  refresh,
310
404
  } = useSearch();
311
405
  ```
@@ -321,20 +415,16 @@ All components accept a `styled` prop (default `true`) that toggles built-in Tai
321
415
  Search input with built-in clear button and search icon.
322
416
 
323
417
  ```tsx
324
- <SearchBox
325
- placeholder="Search products..."
326
- autoFocus
327
- onQueryChange={(q) => console.log(q)}
328
- />
418
+ <SearchBox placeholder="Search products..." autoFocus onQueryChange={(q) => console.log(q)} />
329
419
  ```
330
420
 
331
- | Prop | Type | Default | Description |
332
- | --------------- | --------------------------- | -------------- | ---------------------------- |
333
- | `placeholder` | `string` | `"Search..."` | Input placeholder |
334
- | `className` | `string` | -- | Additional CSS classes |
335
- | `autoFocus` | `boolean` | `false` | Auto-focus on mount |
336
- | `onQueryChange` | `(query: string) => void` | -- | Called on every query change |
337
- | `styled` | `boolean` | `true` | Enable built-in styles |
421
+ | Prop | Type | Default | Description |
422
+ | --------------- | ------------------------- | ------------- | ---------------------------- |
423
+ | `placeholder` | `string` | `"Search..."` | Input placeholder |
424
+ | `className` | `string` | -- | Additional CSS classes |
425
+ | `autoFocus` | `boolean` | `false` | Auto-focus on mount |
426
+ | `onQueryChange` | `(query: string) => void` | -- | Called on every query change |
427
+ | `styled` | `boolean` | `true` | Enable built-in styles |
338
428
 
339
429
  ---
340
430
 
@@ -346,12 +436,12 @@ Renders search result hits in a grid layout.
346
436
  <Hits hitComponent={ProductCard} emptyComponent={<p>No results.</p>} />
347
437
  ```
348
438
 
349
- | Prop | Type | Default | Description |
350
- | ---------------- | ------------------------------- | -------------- | ------------------------------ |
351
- | `hitComponent` | `ComponentType<{ hit: Hit }>` | JSON card | Custom component per hit |
352
- | `className` | `string` | -- | CSS classes for the container |
353
- | `emptyComponent` | `ReactNode` | "No results" | Shown when there are no hits |
354
- | `styled` | `boolean` | `true` | Enable built-in styles |
439
+ | Prop | Type | Default | Description |
440
+ | ---------------- | ----------------------------- | ------------ | ----------------------------- |
441
+ | `hitComponent` | `ComponentType<{ hit: Hit }>` | JSON card | Custom component per hit |
442
+ | `className` | `string` | -- | CSS classes for the container |
443
+ | `emptyComponent` | `ReactNode` | "No results" | Shown when there are no hits |
444
+ | `styled` | `boolean` | `true` | Enable built-in styles |
355
445
 
356
446
  ---
357
447
 
@@ -363,14 +453,14 @@ Checkbox list of facet values with counts.
363
453
  <RefinementList attribute="brand" header="Brand" limit={10} sortBy="count" />
364
454
  ```
365
455
 
366
- | Prop | Type | Default | Description |
367
- | ----------- | -------------------- | --------- | -------------------------------- |
368
- | `attribute` | `string` | -- | Facet attribute |
369
- | `limit` | `number` | `20` | Max values to show |
370
- | `sortBy` | `"count" \| "alpha"` | `"count"` | Value sort order |
371
- | `header` | `string` | -- | Optional heading above the list |
372
- | `className` | `string` | -- | Additional CSS classes |
373
- | `styled` | `boolean` | `true` | Enable built-in styles |
456
+ | Prop | Type | Default | Description |
457
+ | ----------- | -------------------- | --------- | ------------------------------- |
458
+ | `attribute` | `string` | -- | Facet attribute |
459
+ | `limit` | `number` | `20` | Max values to show |
460
+ | `sortBy` | `"count" \| "alpha"` | `"count"` | Value sort order |
461
+ | `header` | `string` | -- | Optional heading above the list |
462
+ | `className` | `string` | -- | Additional CSS classes |
463
+ | `styled` | `boolean` | `true` | Enable built-in styles |
374
464
 
375
465
  ---
376
466
 
@@ -382,10 +472,10 @@ Page navigation with Previous/Next buttons and numbered pages.
382
472
  <Pagination />
383
473
  ```
384
474
 
385
- | Prop | Type | Default | Description |
386
- | ----------- | --------- | ------- | ------------------------ |
387
- | `className` | `string` | -- | Additional CSS classes |
388
- | `styled` | `boolean` | `true` | Enable built-in styles |
475
+ | Prop | Type | Default | Description |
476
+ | ----------- | --------- | ------- | ---------------------- |
477
+ | `className` | `string` | -- | Additional CSS classes |
478
+ | `styled` | `boolean` | `true` | Enable built-in styles |
389
479
 
390
480
  ---
391
481
 
@@ -400,11 +490,11 @@ Displays result count and processing time (e.g. "1,234 results in 3ms").
400
490
  } />
401
491
  ```
402
492
 
403
- | Prop | Type | Default | Description |
404
- | ----------- | ------------------------------------------------------- | ------- | ------------------------- |
405
- | `render` | `(stats: { totalHits; processingTimeMs; query }) => string` | -- | Custom render function |
406
- | `className` | `string` | -- | Additional CSS classes |
407
- | `styled` | `boolean` | `true` | Enable built-in styles |
493
+ | Prop | Type | Default | Description |
494
+ | ----------- | ----------------------------------------------------------- | ------- | ---------------------- |
495
+ | `render` | `(stats: { totalHits; processingTimeMs; query }) => string` | -- | Custom render function |
496
+ | `className` | `string` | -- | Additional CSS classes |
497
+ | `styled` | `boolean` | `true` | Enable built-in styles |
408
498
 
409
499
  ---
410
500
 
@@ -421,12 +511,12 @@ Sort-by select dropdown.
421
511
  />
422
512
  ```
423
513
 
424
- | Prop | Type | Default | Description |
425
- | ----------- | -------------- | ----------- | -------------------------- |
426
- | `items` | `SortByItem[]` | -- | Sort options |
427
- | `label` | `string` | `"Sort by"` | Accessibility label |
428
- | `className` | `string` | -- | Additional CSS classes |
429
- | `styled` | `boolean` | `true` | Enable built-in styles |
514
+ | Prop | Type | Default | Description |
515
+ | ----------- | -------------- | ----------- | ---------------------- |
516
+ | `items` | `SortByItem[]` | -- | Sort options |
517
+ | `label` | `string` | `"Sort by"` | Accessibility label |
518
+ | `className` | `string` | -- | Additional CSS classes |
519
+ | `styled` | `boolean` | `true` | Enable built-in styles |
430
520
 
431
521
  ---
432
522
 
@@ -438,13 +528,13 @@ Renders highlighted text for a hit attribute. Safe for use with `dangerouslySetI
438
528
  <Highlight hit={hit} attribute="title" />
439
529
  ```
440
530
 
441
- | Prop | Type | Default | Description |
442
- | ----------- | ------------------------- | -------- | ----------------------------- |
443
- | `hit` | `Record<string, unknown>` | -- | Hit object from `useHits()` |
444
- | `attribute` | `string` | -- | Attribute to highlight |
445
- | `tagName` | `keyof HTMLElementTagNameMap` | `"span"` | Wrapper element tag |
446
- | `className` | `string` | -- | Additional CSS classes |
447
- | `styled` | `boolean` | `true` | Enable built-in styles |
531
+ | Prop | Type | Default | Description |
532
+ | ----------- | ----------------------------- | -------- | --------------------------- |
533
+ | `hit` | `Record<string, unknown>` | -- | Hit object from `useHits()` |
534
+ | `attribute` | `string` | -- | Attribute to highlight |
535
+ | `tagName` | `keyof HTMLElementTagNameMap` | `"span"` | Wrapper element tag |
536
+ | `className` | `string` | -- | Additional CSS classes |
537
+ | `styled` | `boolean` | `true` | Enable built-in styles |
448
538
 
449
539
  ---
450
540
 
@@ -456,14 +546,14 @@ Min/max number inputs for numeric range filtering.
456
546
  <RangeInput attribute="price" header="Price" />
457
547
  ```
458
548
 
459
- | Prop | Type | Default | Description |
460
- | ---------------- | --------- | ------- | ----------------------------- |
461
- | `attribute` | `string` | -- | Numeric attribute to filter |
462
- | `header` | `string` | -- | Optional heading |
463
- | `minPlaceholder` | `string` | `"Min"` | Min input placeholder |
464
- | `maxPlaceholder` | `string` | `"Max"` | Max input placeholder |
465
- | `className` | `string` | -- | Additional CSS classes |
466
- | `styled` | `boolean` | `true` | Enable built-in styles |
549
+ | Prop | Type | Default | Description |
550
+ | ---------------- | --------- | ------- | --------------------------- |
551
+ | `attribute` | `string` | -- | Numeric attribute to filter |
552
+ | `header` | `string` | -- | Optional heading |
553
+ | `minPlaceholder` | `string` | `"Min"` | Min input placeholder |
554
+ | `maxPlaceholder` | `string` | `"Max"` | Max input placeholder |
555
+ | `className` | `string` | -- | Additional CSS classes |
556
+ | `styled` | `boolean` | `true` | Enable built-in styles |
467
557
 
468
558
  ---
469
559
 
@@ -475,30 +565,30 @@ Min/max number inputs for numeric range filtering.
475
565
  <PoweredBy />
476
566
  ```
477
567
 
478
- | Prop | Type | Default | Description |
479
- | ----------- | --------- | ------------------------ | ------------------------ |
480
- | `href` | `string` | `"https://headband.dev"` | Link URL |
481
- | `className` | `string` | -- | Additional CSS classes |
482
- | `styled` | `boolean` | `true` | Enable built-in styles |
568
+ | Prop | Type | Default | Description |
569
+ | ----------- | --------- | ------------------------ | ---------------------- |
570
+ | `href` | `string` | `"https://headband.dev"` | Link URL |
571
+ | `className` | `string` | -- | Additional CSS classes |
572
+ | `styled` | `boolean` | `true` | Enable built-in styles |
483
573
 
484
574
  ---
485
575
 
486
576
  ## Comparison to Algolia InstantSearch
487
577
 
488
- | Feature | @headband/react | react-instantsearch |
489
- | --------------------- | ---------------------------- | ---------------------------- |
490
- | Provider | `<HeadbandProvider>` | `<InstantSearch>` |
491
- | Search input | `<SearchBox>` / `useSearchBox` | `<SearchBox>` / `useSearchBox` |
492
- | Results | `<Hits>` / `useHits` | `<Hits>` / `useHits` |
493
- | Facets | `<RefinementList>` | `<RefinementList>` |
494
- | Pagination | `<Pagination>` | `<Pagination>` |
495
- | Stats | `<Stats>` | `<Stats>` |
496
- | Sort | `<SortBy>` | `<SortBy>` |
497
- | Range | `<RangeInput>` / `useRange` | `<RangeInput>` / `useRange` |
498
- | Highlighting | `<Highlight>` | `<Highlight>` |
499
- | Bundle size | ~33 KB (ESM, unminified) | ~120 KB+ |
500
- | Backend lock-in | None (Headband is engine-agnostic) | Algolia only |
501
- | Built-in styles | Tailwind (opt-out with `styled={false}`) | CSS classes |
578
+ | Feature | @headband/react | react-instantsearch |
579
+ | --------------- | ---------------------------------------- | ------------------------------ |
580
+ | Provider | `<HeadbandProvider>` | `<InstantSearch>` |
581
+ | Search input | `<SearchBox>` / `useSearchBox` | `<SearchBox>` / `useSearchBox` |
582
+ | Results | `<Hits>` / `useHits` | `<Hits>` / `useHits` |
583
+ | Facets | `<RefinementList>` | `<RefinementList>` |
584
+ | Pagination | `<Pagination>` | `<Pagination>` |
585
+ | Stats | `<Stats>` | `<Stats>` |
586
+ | Sort | `<SortBy>` | `<SortBy>` |
587
+ | Range | `<RangeInput>` / `useRange` | `<RangeInput>` / `useRange` |
588
+ | Highlighting | `<Highlight>` | `<Highlight>` |
589
+ | Bundle size | ~33 KB (ESM, unminified) | ~120 KB+ |
590
+ | Backend lock-in | None (Headband is engine-agnostic) | Algolia only |
591
+ | Built-in styles | Tailwind (opt-out with `styled={false}`) | CSS classes |
502
592
 
503
593
  ## TypeScript
504
594