npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@headband/react

v0.1.1

Published

React hooks and components for Headband search — an InstantSearch-compatible SDK

Readme

@headband/react

React hooks and components for Headband 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.

Installation

npm install @headband/react

Peer dependencies: React 18+ and React DOM 18+.

Quick Start

import {
  headband,
  HeadbandProvider,
  SearchBox,
  Hits,
  RefinementList,
  Pagination,
  Stats,
} from "@headband/react";

const client = headband("https://your-headband-instance.com", "your-api-key");

function App() {
  return (
    <HeadbandProvider client={client} index="movies">
      <SearchBox placeholder="Search movies..." />
      <Stats />
      <div style={{ display: "flex", gap: "2rem" }}>
        <RefinementList attribute="genres" header="Genres" />
        <div>
          <Hits hitComponent={MovieHit} />
          <Pagination />
        </div>
      </div>
    </HeadbandProvider>
  );
}

function MovieHit({ hit }: { hit: any }) {
  return (
    <div>
      <h3>{hit.title}</h3>
      <p>{hit.overview}</p>
    </div>
  );
}

Architecture

All state lives in <HeadbandProvider>, which holds the search query, refinements, pagination, and sort state. Child components read and write state through React hooks. Search requests are debounced (200ms) and stale responses are automatically discarded.

HeadbandProvider          -- owns all search state, executes searches
  |-- useSearchBox()      -- query input binding
  |-- useHits()           -- current result hits
  |-- useRefinementList() -- facet filtering
  |-- usePagination()     -- page navigation
  |-- useStats()          -- result count & timing
  |-- useSortBy()         -- sort order
  |-- useRange()          -- numeric range filters
  |-- useHighlight()      -- highlighted text extraction
  |-- useSearch()         -- full state + actions (escape hatch)

Client

headband(host, apiKey)

Creates a Headband API client.

import { headband } from "@headband/react";

const client = headband("https://search.example.com", "your-api-key");

| Param | Type | Description | | -------- | -------- | --------------------------------- | | host | string | Headband instance URL | | apiKey | string | API key for authentication |

Returns a HeadbandClient with search() and multiSearch() methods.


Provider

<HeadbandProvider>

Wraps your search UI and manages all search state.

<HeadbandProvider
  client={client}
  index="products"
  initialQuery="shoes"
>
  {/* search components */}
</HeadbandProvider>

| Prop | Type | Default | Description | | -------------- | ----------------- | ------- | ---------------------------------------- | | client | HeadbandClient | -- | Client returned by headband() | | index | string | -- | Index to search | | initialQuery | string | "" | Starting query value | | children | ReactNode | -- | Child components |


Hooks

useSearchBox()

Binds to the search input. Returns the current query plus setters.

const { query, setQuery, clear } = useSearchBox();

| Return | Type | Description | | ---------- | -------------------------- | ---------------------------------------------- | | query | string | Current query string | | setQuery | (query: string) => void | Update the query (resets pagination) | | clear | () => void | Clear the query to an empty string |


useHits<T>()

Returns the current search result hits with loading and error state.

const { hits, isLoading, error } = useHits<Movie>();

| Return | Type | Description | | ----------- | ------------- | ---------------------------------------------- | | hits | Hit<T>[] | Array of hit objects | | isLoading | boolean | Whether a search is in progress | | error | Error\|null | Error from the last search, if any |

Each Hit<T> includes the original document fields, an optional _formatted object with highlighted values, and a typed __data: T accessor.


useRefinementList(props)

Provides facet filtering for a given attribute. Automatically registers the attribute so the provider includes it in search requests.

const { items, refine, canToggle, isLoading } = useRefinementList({
  attribute: "brand",
  limit: 10,
  sortBy: "count",
});

| Prop | Type | Default | Description | | ----------- | -------------------- | --------- | ------------------------------------ | | attribute | string | -- | Facet attribute (e.g. "genres") | | limit | number | 20 | Max facet values to show | | sortBy | "count" \| "alpha" | "count" | Sort order for facet values |

| Return | Type | Description | | ----------- | ------------------------ | ------------------------------------- | | items | RefinementListItem[] | Facet values with count & state | | refine | (value: string) => void| Toggle a facet value on/off | | canToggle | boolean | Whether any values are available | | isLoading | boolean | Loading state |

Each RefinementListItem has { value: string; count: number; isRefined: boolean }.


usePagination()

Pagination state derived from the current search results. Pages are 0-indexed.

const { currentPage, totalPages, totalHits, setPage, canPrevious, canNext, pages } =
  usePagination();

| Return | Type | Description | | ------------- | ------------------------- | ------------------------------------ | | currentPage | number | Current page index (0-based) | | totalPages | number | Total number of pages | | totalHits | number | Total result count | | setPage | (page: number) => void | Navigate to a page (clamped) | | canPrevious | boolean | Whether a previous page exists | | canNext | boolean | Whether a next page exists | | pages | number[] | Window of page numbers for rendering |


useStats()

Returns summary statistics about the current search.

const { totalHits, processingTimeMs, query, isLoading } = useStats();

| Return | Type | Description | | ----------------- | --------- | -------------------------------- | | totalHits | number | Total results for the query | | processingTimeMs| number | Server-side processing time (ms) | | query | string | The query that produced results | | isLoading | boolean | Loading state |


useSortBy(props)

Manages sort-by dropdown state.

const { currentSort, setSort, items } = useSortBy({
  items: [
    { value: "price:asc", label: "Price (low to high)" },
    { value: "price:desc", label: "Price (high to low)" },
    { value: "rating:desc", label: "Top rated" },
  ],
});

| Prop | Type | Description | | ------- | -------------- | ----------------------------------------- | | items | SortByItem[] | Available sort options |

Each SortByItem has { value: string; label: string }.

| Return | Type | Description | | ------------- | --------------------------------- | ------------------------------ | | currentSort | string \| null | Active sort value (null = relevance) | | setSort | (value: string\|null) => void | Set the sort order | | items | (SortByItem & { isSelected })[] | Items with isSelected flag |


useRange(props)

Numeric range filter for a given attribute.

const { min, max, stats, setRange, clear, isLoading } = useRange({
  attribute: "price",
});

| Prop | Type | Description | | ----------- | -------- | ------------------------------------- | | attribute | string | Numeric attribute (e.g. "price") |

| Return | Type | Description | | ----------- | ------------------------------------------ | -------------------------------- | | min | number \| undefined | Current min bound | | max | number \| undefined | Current max bound | | stats | { min: number; max: number } \| undefined| Overall min/max from engine | | setRange | (range: { min?: number; max?: number }) => void | Set range filter | | clear | () => void | Remove the range filter | | isLoading | boolean | Loading state |


useHighlight(props)

Extracts the highlighted value for a specific attribute from a hit. Sanitizes the HTML to only allow <em> tags for safety.

const { value, highlighted } = useHighlight({
  hit: someHit,
  attribute: "title",
});

| Prop | Type | Description | | ----------- | ------------------------ | ---------------------------------- | | hit | Record<string, unknown>| The hit object from useHits() | | attribute | string | Attribute to highlight |

| Return | Type | Description | | ------------- | --------- | ---------------------------------------------- | | value | string | Highlighted HTML string (safe for innerHTML) | | highlighted | boolean | Whether the value contains highlight marks |

Supports nested attributes via dot notation (e.g. "author.name").


useSearch()

Escape-hatch hook that exposes the full search state and all actions. Prefer the specialized hooks above for most use cases.

const {
  query, results, isLoading, error, refinements, ranges, sortBy, page,
  hitsPerPage, facets, setQuery, setPage, setSortBy, setHitsPerPage,
  toggleRefinement, setRange, clearRange, registerFacet, unregisterFacet,
  refresh,
} = useSearch();

Components

All components accept a styled prop (default true) that toggles built-in Tailwind CSS classes. Set styled={false} to bring your own styles.

<SearchBox>

Search input with built-in clear button and search icon.

<SearchBox
  placeholder="Search products..."
  autoFocus
  onQueryChange={(q) => console.log(q)}
/>

| Prop | Type | Default | Description | | --------------- | --------------------------- | -------------- | ---------------------------- | | placeholder | string | "Search..." | Input placeholder | | className | string | -- | Additional CSS classes | | autoFocus | boolean | false | Auto-focus on mount | | onQueryChange | (query: string) => void | -- | Called on every query change | | styled | boolean | true | Enable built-in styles |


<Hits>

Renders search result hits in a grid layout.

<Hits hitComponent={ProductCard} emptyComponent={<p>No results.</p>} />

| Prop | Type | Default | Description | | ---------------- | ------------------------------- | -------------- | ------------------------------ | | hitComponent | ComponentType<{ hit: Hit }> | JSON card | Custom component per hit | | className | string | -- | CSS classes for the container | | emptyComponent | ReactNode | "No results" | Shown when there are no hits | | styled | boolean | true | Enable built-in styles |


<RefinementList>

Checkbox list of facet values with counts.

<RefinementList attribute="brand" header="Brand" limit={10} sortBy="count" />

| Prop | Type | Default | Description | | ----------- | -------------------- | --------- | -------------------------------- | | attribute | string | -- | Facet attribute | | limit | number | 20 | Max values to show | | sortBy | "count" \| "alpha" | "count" | Value sort order | | header | string | -- | Optional heading above the list | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<Pagination>

Page navigation with Previous/Next buttons and numbered pages.

<Pagination />

| Prop | Type | Default | Description | | ----------- | --------- | ------- | ------------------------ | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<Stats>

Displays result count and processing time (e.g. "1,234 results in 3ms").

<Stats />
<Stats render={({ totalHits, processingTimeMs }) =>
  `Found ${totalHits} items (${processingTimeMs}ms)`
} />

| Prop | Type | Default | Description | | ----------- | ------------------------------------------------------- | ------- | ------------------------- | | render | (stats: { totalHits; processingTimeMs; query }) => string | -- | Custom render function | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<SortBy>

Sort-by select dropdown.

<SortBy
  items={[
    { value: "price:asc", label: "Price (low to high)" },
    { value: "price:desc", label: "Price (high to low)" },
  ]}
/>

| Prop | Type | Default | Description | | ----------- | -------------- | ----------- | -------------------------- | | items | SortByItem[] | -- | Sort options | | label | string | "Sort by" | Accessibility label | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<Highlight>

Renders highlighted text for a hit attribute. Safe for use with dangerouslySetInnerHTML -- all HTML except <em> tags is stripped and escaped.

<Highlight hit={hit} attribute="title" />

| Prop | Type | Default | Description | | ----------- | ------------------------- | -------- | ----------------------------- | | hit | Record<string, unknown> | -- | Hit object from useHits() | | attribute | string | -- | Attribute to highlight | | tagName | keyof HTMLElementTagNameMap | "span" | Wrapper element tag | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<RangeInput>

Min/max number inputs for numeric range filtering.

<RangeInput attribute="price" header="Price" />

| Prop | Type | Default | Description | | ---------------- | --------- | ------- | ----------------------------- | | attribute | string | -- | Numeric attribute to filter | | header | string | -- | Optional heading | | minPlaceholder | string | "Min" | Min input placeholder | | maxPlaceholder | string | "Max" | Max input placeholder | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


<PoweredBy>

"Powered by Headband" badge with link.

<PoweredBy />

| Prop | Type | Default | Description | | ----------- | --------- | ------------------------ | ------------------------ | | href | string | "https://headband.dev" | Link URL | | className | string | -- | Additional CSS classes | | styled | boolean | true | Enable built-in styles |


Comparison to Algolia InstantSearch

| Feature | @headband/react | react-instantsearch | | --------------------- | ---------------------------- | ---------------------------- | | Provider | <HeadbandProvider> | <InstantSearch> | | Search input | <SearchBox> / useSearchBox | <SearchBox> / useSearchBox | | Results | <Hits> / useHits | <Hits> / useHits | | Facets | <RefinementList> | <RefinementList> | | Pagination | <Pagination> | <Pagination> | | Stats | <Stats> | <Stats> | | Sort | <SortBy> | <SortBy> | | Range | <RangeInput> / useRange | <RangeInput> / useRange | | Highlighting | <Highlight> | <Highlight> | | Bundle size | ~33 KB (ESM, unminified) | ~120 KB+ | | Backend lock-in | None (Headband is engine-agnostic) | Algolia only | | Built-in styles | Tailwind (opt-out with styled={false}) | CSS classes |

TypeScript

All hooks, components, and types are fully typed and exported. Import types directly:

import type {
  HeadbandClient,
  SearchParams,
  SearchResponse,
  Hit,
  RefinementListItem,
  SortByItem,
} from "@headband/react";

License

MIT