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

@etoile-dev/react

v1.0.2

Published

Official React primitives for Etoile - Headless, composable search components

Readme


About

@etoile-dev/react is the React SDK for Etoile, and also a set of unstyled primitives you can wire to any data source.

You get three layers:

  • Ready-to-use Etoile components<Searchbar /> and <SearchModal />
  • Etoile data hooksuseEtoileSearch (and useSearch alias)
  • Headless primitivesSearchbar.Root, Searchbar.Input, Searchbar.List, Searchbar.Item, …

Philosophy

  • Headless-first — You control the appearance
  • Composable — Build any search UX from small primitives
  • Accessible — Full ARIA combobox / listbox pattern, keyboard navigation
  • No magic — Predictable behavior, clear contracts
  • Zero style opinions — Bring your own CSS (or use our optional theme)

Install

npm i @etoile-dev/react

Quickstart

The simplest possible usage — just an API key and a collection:

import "@etoile-dev/react/styles.css";
import { Searchbar } from "@etoile-dev/react";

export default function App() {
  return <Searchbar apiKey="your-api-key" collections={["paintings"]} />;
}

Search multiple collections and handle selection:

<Searchbar
  apiKey={process.env.ETOILE_API_KEY!}
  collections={["paintings", "artists"]}
  limit={10}
  onSelect={(id) => router.push(`/work/${id}`)}
/>

Headless primitives

Use Searchbar.Root and friends for full control with no Etoile dependency:

import { Searchbar } from "@etoile-dev/react";

export default function LocalSearch() {
  return (
    <Searchbar.Root onSelect={(id) => router.push(`/paintings/${id}`)}>
      <Searchbar.Input placeholder="Search paintings…" />
      <Searchbar.List>
        {paintings.map((p) => (
          <Searchbar.Item key={p.id} value={p.id} label={p.title}>
            {p.title}
          </Searchbar.Item>
        ))}
        <Searchbar.Empty>No results.</Searchbar.Empty>
        <Searchbar.Loading />
      </Searchbar.List>
    </Searchbar.Root>
  );
}

Primitives are unstyled by default. To opt into the built-in theme while using primitives, add className="etoile-search" on Searchbar.Root and import @etoile-dev/react/styles.css.

For modal compositions, apply the theme class once on Searchbar.Root. Searchbar.Overlay and Searchbar.Content inherit it automatically.


Custom rendering

<Searchbar
  apiKey={process.env.ETOILE_API_KEY!}
  collections={["paintings", "artists"]}
  onSelectResult={(result) => router.push(`/work/${result.external_id}`)}
  renderItem={(result) => (
    <Searchbar.Item value={result.external_id} label={result.title}>
      <Searchbar.Thumbnail />
      <div>
        <strong>{result.title}</strong>
        <span>{String(result.metadata?.artist ?? "")}</span>
      </div>
    </Searchbar.Item>
  )}
/>

Or use a fully custom link strategy in your item renderer:

<Searchbar.Item value={result.external_id} label={result.title}>
  <a href={String(result.metadata?.linkUrl ?? `/work/${result.external_id}`)}>
    {result.title}
  </a>
</Searchbar.Item>

Command palette / modal mode

Use the <SearchModal /> convenience component for an Etoile-powered palette:

import "@etoile-dev/react/styles.css";
import { SearchModal } from "@etoile-dev/react";

<SearchModal apiKey="your-api-key" collections={["paintings"]} />;

Or compose the primitives yourself for full control:

<Searchbar.Root className="etoile-search">
  <Searchbar.Trigger>
    <Searchbar.Icon /> Search
  </Searchbar.Trigger>

  <Searchbar.Portal>
    <Searchbar.Overlay className="overlay" />
    <Searchbar.Content aria-label="Search paintings" className="palette">
      <Searchbar.Input autoFocus placeholder="Search…" />
      <Searchbar.List>
        {results.map((r) => (
          <Searchbar.Item key={r.id} value={r.id}>
            {r.title}
          </Searchbar.Item>
        ))}
        <Searchbar.Empty>No results.</Searchbar.Empty>
      </Searchbar.List>
    </Searchbar.Content>
  </Searchbar.Portal>
</Searchbar.Root>

Styling

Data attributes

All primitives emit data-* attributes — no class coupling required:

[role="option"][data-selected="true"] {
  background: #f0f9ff;
}

[role="option"][data-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

[data-state="open"] {
  border-color: #3b82f6;
}

Dark mode

<Searchbar className="dark" apiKey="your-api-key" collections={["paintings"]} />

Or wrap a parent element:

<div className="dark">
  <Searchbar apiKey="your-api-key" collections={["paintings"]} />
</div>

CSS variables

Every value is customizable:

.etoile-search {
  --etoile-bg: #ffffff;
  --etoile-border: #e4e4e7;
  --etoile-text: #09090b;
  --etoile-text-muted: #71717a;
  --etoile-selected: #f4f4f5;
  --etoile-radius: 12px;
  --etoile-input-height: 44px;
}

See styles.css for all 40+ variables.


API

<Searchbar />

Ready-to-use search component powered by Etoile.

| Prop | Type | Default | | ------------- | ------------------------------------- | ----------------- | | apiKey | string | required | | collections | string[] | required | | limit | number | 10 | | offset | number | 0 (API default) | | debounceMs | number | 100 | | placeholder | string | "Search…" | | filters | SearchFilter[] | | | autoFilters | boolean | | | renderItem | (result: SearchResult) => ReactNode | | | onSelect | (value: string) => void | | | onSelectResult | (result: SearchResult) => void | | | hotkey | string | | | className | string | "etoile-search" |

Also supports non-state DOM/behavior props from Searchbar.Root. Use onSelectResult for simple routing with external_id.


<SearchModal />

Ready-to-use command palette powered by Etoile.

| Prop | Type | Default | | ------------- | ------------------------------------- | ----------------- | | apiKey | string | required | | collections | string[] | required | | limit | number | 10 | | offset | number | 0 (API default) | | debounceMs | number | 100 | | placeholder | string | "Search…" | | filters | SearchFilter[] | | | autoFilters | boolean | | | hotkey | string | "mod+k" | | modalLabel | string | "Search" | | renderItem | (result: SearchResult) => ReactNode | | | onSelect | (value: string) => void | | | onSelectResult | (result: SearchResult) => void | | | className | string | "etoile-search" |

Use onSelectResult for simple routing with external_id.


<Searchbar.Root />

Context provider and state machine for headless usage.

| Prop | Type | Default | | ---------------- | --------------------------------- | ---------- | | open | boolean | | | defaultOpen | boolean | false | | onOpenChange | (open: boolean) => void | | | search | string | | | defaultSearch | string | "" | | onSearchChange | (search: string) => void | | | value | string \| null | | | defaultValue | string \| null | null | | onValueChange | (value: string \| null) => void | | | isLoading | boolean | false | | error | unknown | | | hotkey | string | | | hotkeyBehavior | "focus" \| "toggle" | "toggle" | | onSelect | (value: string) => void | | | themeClassName | string | | | className | string | | | asChild | boolean | false |

Keyboard shortcuts:

  • / — Navigate items
  • Enter — Select active item
  • Escape — Close list

<Searchbar.Modal />

Headless modal primitive (Root + Portal + Overlay + Content).

| Prop | Type | Default | | ------------ | -------- | ---------- | | aria-label | string | "Search" |

Also accepts Searchbar.Root props.


<Searchbar.Input />

| Prop | Type | Default | | ------------- | --------- | ------- | | placeholder | string | | | asChild | boolean | false |


<Searchbar.ModalInput />

Pre-composed modal input row.

| Prop | Type | Default | | ------------- | ------------------- | ----------- | | placeholder | string | "Search…" | | icon | ReactNode \| null | <Icon /> | | kbd | ReactNode \| null | "Esc" |


<Searchbar.List />

Container with role="listbox". Renders when open. In modal mode, it hides when query is empty.

| Prop | Type | Default | | --------- | --------- | ------- | | asChild | boolean | false |


<Searchbar.Results />

Helper primitive to render arrays of results with optional built-in states.

<Searchbar.List>
  <Searchbar.Results
    results={results}
    renderItem={(result) => (
      <Searchbar.Item value={result.external_id} label={result.title}>
        {result.title}
      </Searchbar.Item>
    )}
  />
</Searchbar.List>

| Prop | Type | | ------------ | --------------------------------------------- | | results | T[] | | renderItem | (result: T, index: number) => ReactNode | | getValue | (result: T, index: number) => string | | getLabel | (result: T, index: number) => string | | empty | ReactNode \| null | | loading | ReactNode \| null | | error | ReactNode \| ((error: unknown) => ReactNode) \| null |


<Searchbar.Item />

| Prop | Type | Default | | ---------- | ------------------------- | -------- | | value | string | required | | label | string | value | | disabled | boolean | false | | onSelect | (value: string) => void | | | asChild | boolean | false |

Data attributes: data-selected, data-disabled, data-value


<Searchbar.Group />

| Prop | Type | | ------- | -------- | | label | string |


<Searchbar.Separator />

Visual separator (role="separator").


<Searchbar.Empty />

Renders when: list is open, query is non-empty, no items match, and not loading.


<Searchbar.Loading />

Renders when isLoading={true} is passed to Searchbar.Root.


<Searchbar.Error />

Renders when error is set on Searchbar.Root. Accepts a render function:

<Searchbar.Error>{(err) => `Search failed: ${String(err)}`}</Searchbar.Error>

<Searchbar.Portal />

Portals children to document.body (or a custom container).


<Searchbar.Overlay />

Backdrop for modal/palette mode. Renders when open.


<Searchbar.Content />

Dialog panel for modal/palette mode. Focuses first focusable child on open.


<Searchbar.Trigger />

Button that toggles open state.


<Searchbar.Icon />

Search magnifying glass SVG icon.

| Prop | Type | Default | | ------ | -------- | ------- | | size | number | 18 |


<Searchbar.Kbd />

Keyboard shortcut badge.

<Searchbar.Kbd />          // "⌘K"
<Searchbar.Kbd>/</Searchbar.Kbd>

<Searchbar.Thumbnail />

Thumbnail image. Auto-reads metadata.thumbnailUrl from item context when used inside the Etoile <Searchbar /> wrapper.

| Prop | Type | Default | | ------ | -------- | ------------ | | src | string | from context | | alt | string | item title | | size | number | 40 |


useSearchbarContext()

Access store helpers from any component inside Searchbar.Root.

import { useSearchbarContext, useSearchbarStore } from "@etoile-dev/react";

function QueryDisplay() {
  const { store } = useSearchbarContext();
  const query = useSearchbarStore(store, (s) => s.query);
  return <span>{query}</span>;
}

useEtoileSearch(options)

Headless data hook for live Etoile search.

import { useEtoileSearch } from "@etoile-dev/react";

const [query, setQuery] = useState("");
const { results, isLoading } = useEtoileSearch({
  apiKey: process.env.ETOILE_API_KEY!,
  collections: ["paintings"],
  query,
  shouldRetryOnError: true,
  errorRetryCount: 2,
  errorRetryInterval: 1000,
});

Returns results, isLoading, error, isError, appliedFilters, and refinedQuery.


Types

import type {
  SearchResult,
  SearchFilter,
  FilterOperator,
} from "@etoile-dev/react";

SearchResultData remains exported as a deprecated alias.


Why @etoile-dev/react?

  • Fast by default — only the components that depend on the query re-render, not the whole tree
  • Stable selection — items are identified by value, not by index
  • Controlled or uncontrolled — every stateful prop supports both patterns
  • Composable — render any element as any primitive with asChild
  • No opinions — primitives emit data-* attributes and inject no theme classes