> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hibonsai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React & Next.js

> Installing the Bonsai web components in a React or Next.js App Router project

The web components are framework-agnostic custom elements, so React renders them as ordinary tags and the SDK script upgrades them in the browser. Nothing needs a wrapper library.

This page walks a complete install on a **Next.js App Router** project with a dark storefront. Adapt the palette and the route names; everything else transfers.

## Before you start

Complete the [Quickstart prerequisites](/project/docs/quickstart#prerequisites), and pay particular attention to origin registration — it is the step that most often blocks a framework install:

<Callout color="#B45309" icon="triangle-alert">
  **Origins are matched as exact strings.** On a platform that mints a new hostname per deployment — Vercel and Netlify preview URLs, for example — every preview origin is a *different* origin and will be refused. Register the stable alias and develop against that, or against a registered `localhost` port. A refused origin surfaces in the component as `Failed to fetch`.
</Callout>

## 1. Load the script once, in the root layout

One bundle registers **both** `<bonsai-search>` and `<bonsai-searchbar>`, so a single tag in the root layout covers every page. Do not add a second script for the second component.

```tsx theme={null}
// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://assets.hibonsai.com/sdk/bonsai-search-webcomponent-latest.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
```

## 2. Declare the tags for TypeScript

Without this, JSX reports the custom elements as unknown. Declare them once.

```ts theme={null}
// types/bonsai.d.ts
declare namespace React.JSX {
  interface IntrinsicElements {
    "bonsai-search": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> &
      { "api-key": string; theme?: string; "search-path"?: string; markdown?: string };
    "bonsai-searchbar": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> &
      { "api-key": string; theme?: string; "search-path"?: string };
  }
}
```

<Callout color="#0A5B3B" icon="circle-alert">
  Neither component needs `"use client"`. They are plain tags in the server-rendered HTML, and the script upgrades them in the browser. Add `"use client"` only if that particular component also attaches [event listeners](/project/docs/bonsai-search/events) or calls the imperative methods.
</Callout>

## 3. Put the palette in global CSS

Set the variables on the elements themselves — a value inherited from `:root` or a wrapper loses to the SDK's own `:host` declaration.

The block below is a dark palette on a pure black canvas with a monospace face. `theme="light"` on the tags in the next steps is what keeps these lines editable; see [Dark theme](/project/docs/bonsai-search/styling#dark-theme).

```css theme={null}
/* app/globals.css */
bonsai-search,
bonsai-searchbar {
  display: block;
  width: 100%;
  --bonsai-font-body: robotoMono, ui-monospace, monospace;
  --bonsai-font-heading: robotoMono, ui-monospace, monospace;
  --bonsai-font-mono: robotoMono, ui-monospace, monospace;
  --bonsai-search-max-width: 100%;
  --bonsai-image-object-fit: cover;

  --bonsai-brand-color: #ffffff;
  --bonsai-text-color: #ffffff;
  --bonsai-results-text-color: #ffffff;
  --bonsai-card-text-color: #ffffff;
  --bonsai-card-bg: transparent;
  --bonsai-surface-color: #0a0a0a;
  --bonsai-muted-color: #8a8a8a;
  --bonsai-input-bg: #ffffff;
  --bonsai-input-text-color: #111111;
  --bonsai-suggestions-text-color: #111111;
  --bonsai-suggestions-hover-bg: rgba(0, 0, 0, 0.06);

  --bonsai-border-color: rgba(255, 255, 255, 0.12);
  --bonsai-border-color-hover: rgba(255, 255, 255, 0.24);
  --bonsai-hover-bg: rgba(255, 255, 255, 0.06);
  --bonsai-error-bg: rgba(248, 113, 113, 0.15);
  --bonsai-error-color: #fca5a5;
  --bonsai-skeleton-base: rgba(255, 255, 255, 0.04);
  --bonsai-skeleton-highlight: rgba(255, 255, 255, 0.08);
  --bonsai-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
  --bonsai-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.55), 0 2px 4px -2px rgb(0 0 0 / 0.5);
  --bonsai-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.6), 0 4px 6px -4px rgb(0 0 0 / 0.55);
}
```

## 4. The search bar, in your header

```tsx theme={null}
<bonsai-searchbar
  api-key="API-KEY"
  search-path="/ai-search"
  theme="light"
/>
```

On submit it navigates to `/ai-search?q=…`. Placeholder text and suggestion chips come from the Settings API, so leave them off the tag and manage them in the dashboard.

To put the bar behind a trigger instead — an icon or an "AI" button in the header — add `close-button` and listen for the [`close` event](/project/docs/bonsai-searchbar/events), which is where that pattern is written out in full.

## 5. The results route

`<bonsai-search>` reads `?q=` from the URL itself, prefills the input and runs the search. There is nothing to wire between the two components — the search bar's `search-path` just has to point at this route.

```tsx theme={null}
// app/ai-search/page.tsx
export const metadata = { title: "AI Search" };

export default function AiSearchPage() {
  return (
    <main style={{ maxWidth: "48rem", margin: "4rem auto", padding: "0 1.5rem" }}>
      <bonsai-search
        api-key="API-KEY"
        theme="light"
        featured-items-label="Featured Items"
        items-label="ITEMS"
      />
    </main>
  );
}
```

No `base-url`: the API origin defaults to production and the path comes from your organization's configured search version. See [`base-url`](/project/docs/bonsai-search/config#api-configuration) if you need to pin one.

## 6. The pixel

Add the [pixel](/project/docs/pixel) to the same root layout with `strategy="beforeInteractive"`, so it runs before the components look for a shopper identifier. Set `cookieDomain` to your registrable domain once the app is on its real host; pass `""` while it is still on a platform subdomain.

## Reading results in React

If you need the results in your own component — analytics, a custom empty state — listen for the [events](/project/docs/bonsai-search/events). They bubble and are composed, so a ref on the element or a listener on an ancestor both work. This is the case that needs `"use client"`.

```tsx theme={null}
"use client";
import { useEffect, useRef } from "react";

export function SearchWithTracking() {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const onResults = (e: Event) => {
      const { query, results } = (e as CustomEvent).detail;
      console.log(`${results.length} results for ${query}`);
    };
    el.addEventListener("results", onResults);
    return () => el.removeEventListener("results", onResults);
  }, []);

  return <bonsai-search ref={ref} api-key="API-KEY" theme="light" />;
}
```

## Vite, Remix, and plain React

The same five pieces apply, only the script tag moves. Put it in `index.html` for Vite, in the root route's `<Links>`/`<Scripts>` region for Remix, or in `public/index.html` for Create React App. The TypeScript declaration, the CSS, the tags and the `?q=` contract are identical.
