Official React primitives for Étoile - Headless, composable search components
npm install @etoile-dev/react

Headless React primitives for search.
Composable. Accessible. Zero styling.
---
@etoile-dev/react provides headless, composable React components for search powered by Étoile.
Built on top of @etoile-dev/client, these primitives give you full control over styling while handling state, keyboard navigation, and accessibility.
---
- Headless-first — You control the appearance
- Composable — Build your own search UX
- Accessible — Full ARIA support and keyboard navigation
- No magic — Behavior is predictable and documented
- No opinions — Bring your own styles (or use our optional theme)
---
``bash`
npm i @etoile-dev/react
---
`tsx
import { Search } from "@etoile-dev/react";
export default function App() {
return
}
`
---
For full control, use the headless primitives:
`tsx
import {
SearchRoot,
SearchInput,
SearchResults,
SearchResult,
} from "@etoile-dev/react";
export default function CustomSearch() { {result.metadata.artist}
return (
collections={["paintings"]}
limit={20}
>
{(result) => (
{result.title}
Score: {result.score.toFixed(2)}
)}
);
}
`
---
Each result automatically gets data-selected and data-index attributes:
`css
.result-item {
padding: 1rem;
cursor: pointer;
}
.result-item[data-selected="true"] {
background: #f0f9ff;
border-left: 3px solid #0ea5e9;
}
`
---
Import the optional theme for a polished, ready-to-use experience:
`tsx
import "@etoile-dev/react/styles.css";
import { Search } from "@etoile-dev/react";
`
That's it! The etoile-search class is applied automatically.
Add dark to the className:
`tsx
// Or with SearchRoot
...
`
Every value is customizable. Here are the key variables:
`css
.etoile-search {
/ Colors /
--etoile-bg: #ffffff;
--etoile-border: #e4e4e7;
--etoile-text: #09090b;
--etoile-text-muted: #71717a;
--etoile-selected: #f4f4f5;
--etoile-ring: #18181b;
/ Sizing /
--etoile-radius: 12px;
--etoile-input-height: 44px;
--etoile-thumbnail-size: 40px;
--etoile-results-max-height: 300px;
/ Spacing /
--etoile-input-padding-x: 16px;
--etoile-result-gap: 16px;
--etoile-results-offset: 8px;
/ Typography /
--etoile-font-size-input: 15px;
--etoile-font-size-title: 14px;
/ Animation /
--etoile-transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
`
See styles.css for the complete list of 40+ variables.
---
For complete control, use the useSearch hook:
`tsx
import { useSearch } from "@etoile-dev/react";
function MyCustomSearch() {
const { query, setQuery, results, isLoading } = useSearch({
apiKey: "your-api-key",
collections: ["paintings"],
});
return (
Loading...
}---
API
$3
Convenience component that composes all primitives.
| Prop | Type | Required | Default |
|----------------|-----------------------------------------------|----------|---------|
|
apiKey | string | ✓ | |
| collections | string[] | ✓ | |
| limit | number | | 10 |
| renderResult | (result: SearchResultData) => React.ReactNode | | |---
$3
Context provider that manages search state and keyboard navigation.
| Prop | Type | Required | Default |
|---------------|-------------------|----------|---------|
|
apiKey | string | ✓ | |
| collections | string[] | ✓ | |
| limit | number | | 10 |
| debounceMs | number | | 100 |
| autoFocus | boolean | | false |
| children | React.ReactNode | ✓ | |---
$3
Controlled input with ARIA combobox role.
| Prop | Type |
|---------------|----------|
|
placeholder | string |
| className | string |Keyboard shortcuts:
-
ArrowUp / ArrowDown — Navigate results
- Enter — Select active result
- Escape — Close results (press again to clear)---
$3
Results container with ARIA listbox role.
| Prop | Type | Required |
|-------------|-----------------------------------------------|----------|
|
className | string | |
| children | (result: SearchResultData) => React.ReactNode | ✓ |---
$3
Individual result with ARIA option role.
| Prop | Type | Required |
|-------------|-------------------|----------|
|
className | string | |
| children | React.ReactNode | ✓ |Data attributes:
-
data-selected="true" | "false" — Active state
- data-index="number" — Result position---
$3
Thumbnail image that auto-detects from
metadata.thumbnailUrl.| Prop | Type | Required | Default |
|-------------|----------|----------|-------------------------------|
|
src | string | | metadata.thumbnailUrl |
| alt | string | | result.title |
| size | number | | 40 |
| className | string | | |---
$3
Built-in search magnifying glass SVG icon.
| Prop | Type | Required | Default |
|-------------|----------|----------|---------|
|
size | number | | 18 |
| className | string | | |---
$3
Keyboard shortcut badge.
| Prop | Type | Required | Default |
|-------------|-------------------|----------|---------|
|
children | React.ReactNode | | ⌘K |
| className | string | | etoile-kbd |`tsx
// Shows "⌘K"
/ // Shows "/"
`---
$3
Headless hook for complete control.
Options:
| Field | Type | Required | Default |
|---------------|------------|----------|---------|
|
apiKey | string | ✓ | |
| collections | string[] | ✓ | |
| limit | number | | 10 |
| debounceMs | number | | 100 |Returns:
| Field | Type |
|--------------------|----------------------------|
|
query | string |
| setQuery | (q: string) => void |
| results | SearchResultData[] |
| isLoading | boolean |
| selectedIndex | number |
| setSelectedIndex | (i: number) => void |
| clear | () => void |---
Types
`ts
type SearchResultData = {
external_id: string;
title: string;
collection: string;
score: number;
content?: string;
metadata: Record;
};
``---
- Radix / shadcn-style primitives — Composable and unstyled
- Accessibility built-in — ARIA combobox, keyboard navigation, focus management, click-outside dismiss
- Behavior, not appearance — You own the design
- TypeScript-first — Full type safety
- Zero dependencies — Only React and @etoile-dev/client
---