Skip to main content

AI Search React/TSX Implementation

Log in to add to favourites

Page last updated 28 July 2026

This guide walks through integrating the insytful-ai-search-components modal widget into an existing React application, from install to production-ready configuration.

1. Install

JavaScript
npm install insytful-ai-search-components

Peer dependencies — make sure your project already has these:

JavaScript
npm install react react-dom

(react and react-dom >=17 are required; the package does not bundle its own copies.)

No CSS import step is needed — styles ship inside a Shadow DOM that the component injects itself, so they can't leak into or be overridden by your app's global CSS by accident.

2. Minimal working example

tsx
import { useState } from "react";
import { InsytfulSearch } from "insytful-ai-search-components";

function SiteSearch() {
  const [open, setOpen] = useState(false);

  return (
    <InsytfulSearch.Root
      options={{ config: "your-config", baseUrl: "https://api.example.com" }}
      open={open}
      onOpenChange={setOpen}
    >
      <InsytfulSearch.Trigger>Search</InsytfulSearch.Trigger>

      <InsytfulSearch.Portal>
        <InsytfulSearch.Title>How can we help?</InsytfulSearch.Title>
        <InsytfulSearch.Description>
          Ask a question and get an AI-generated answer.
        </InsytfulSearch.Description>
        <InsytfulSearch.Input placeholder="Ask a question..." />
        <InsytfulSearch.Messages />
        <InsytfulSearch.ErrorCallout />
        <InsytfulSearch.Suggestions
          items={["How do I get started?", "What does this product do?"]}
        />
        <InsytfulSearch.Disclaimer>
          AI-generated answers may be inaccurate.
        </InsytfulSearch.Disclaimer>
      </InsytfulSearch.Portal>
    </InsytfulSearch.Root>
  );
}

Drop <SiteSearch /> anywhere in your tree — the Trigger renders in place (wherever you put it, e.g. your header), while Portal mounts its dialog into a Shadow DOM host attached directly to document.body, so it isn't affected by your app's z-index/overflow/stacking context.

What each piece does

ComponentRole
RootOwns state and config. Everything else must be a descendant.
TriggerA button that opens the dialog. Renders wherever you place it — doesn't have to be near Portal.
PortalBoundary into the Shadow DOM. Everything inside it is the actual dialog content.
Title / DescriptionAccessible heading/description, wired to aria-labelledby/aria-describedby automatically.
InputThe question textarea + send button.
MessagesRenders the streaming conversation transcript.
ErrorCalloutShown automatically when a request fails; customisable content.
SuggestionsOptional clickable starter prompts.
DisclaimerOptional static text under the input.
CloseOptional explicit close button (place inside Portal if you want one besides Escape/outside-click).

You can omit any of Title, Description, Suggestions, Disclaimer, Close — only Root, Trigger (or another way to call setOpen(true)), Portal, Input, and Messages are load-bearing for a working search experience.

3. Required configuration

options on Root is required and needs at minimum:

tsx
options={{
  config: "your-config",              // Insytful config identifier
  baseUrl: "https://api.example.com", // your Insytful API base URL
}}

Get these values from your account manager, or contact support. If you don't have a live backend yet, skip to Section 6 (Dev mode) — you can build and demo the UI without one.

4. Controlling open state

open/onOpenChange is a standard controlled pair. You can:

  • Let it manage its own state — omit open/onOpenChange entirely and the component manages open/closed internally via Trigger.
  • Control it externally — pass open/onOpenChange if you need to open the search from elsewhere in your app (e.g. a keyboard shortcut, a nav link, a "no results" empty state elsewhere on the page):
tsx
const [open, setOpen] = useState(false);

// Somewhere else in your app:
<button onClick={() => setOpen(true)}>Ask AI</button>

<InsytfulSearch.Root open={open} onOpenChange={setOpen} options={...}>
  {/* Trigger is optional if you're opening it externally */}
  <InsytfulSearch.Portal>...</InsytfulSearch.Portal>
</InsytfulSearch.Root>

Conversation state (the message transcript) persists across close/reopen within the same page session — closing doesn't reset the conversation, since the dialog is hidden via CSS rather than unmounted.

5. Modal vs. widget layout

Root accepts a variant prop:

tsx
<InsytfulSearch.Root variant="modal" ...>   {/* default: full-bleed centered dialog */}
<InsytfulSearch.Root variant="widget" ...>  {/* corner-anchored floating panel */}
  • modal (default) — centered, full-bleed dialog, good for a primary "search this site" experience triggered from a header/nav button.
  • widget — a small floating panel anchored to a screen corner, sized via CSS custom properties. Good for an always-available "chat with AI" affordance that doesn't take over the screen.

Pick based on how prominent you want the entry point to be. Both share the same Root/Trigger/Portal API — only the visual chrome and positioning differ.

6. Local development without a live backend

Pass isDevMode to Root and the package auto-mocks requests, streaming back a markdown response as real SSE frames (with a short artificial delay so you can see the loading skeleton):

tsx
<InsytfulSearch.Root
  options={{ config: "dev", baseUrl: "https://api.example.com" }}
  isDevMode={process.env.NODE_ENV === "development"}
>
  ...
</InsytfulSearch.Root>

This lets front-end work (layout, theming, copy) proceed independently of backend availability. Make sure isDevMode is false/omitted in production builds — gate it behind an env check as shown above, not a hardcoded true.

7. Theming

Two layers, use whichever fits:

  1. CSS custom properties — set on your app's :root (they pierce the Shadow DOM automatically since custom properties inherit across shadow boundaries):
  2. theme prop — a raw CSS string injected directly into the component's Shadow DOM for cases custom properties don't cover: Only pass developer-authored CSS here — this string is injected verbatim (not sanitized), so never pass user-generated or untrusted content through theme.

Start with CSS custom properties; only reach for theme if you need to target internal selectors not exposed as variables.

8. Rendering AI responses as markdown

By default, response text renders as plain text. To render markdown (headings, lists, links, code blocks), pass a renderMarkdown function to Messages:

tsx
import ReactMarkdown from "react-markdown";

<InsytfulSearch.Messages renderMarkdown={(content) => <ReactMarkdown>{content}</ReactMarkdown>} />

The package doesn't bundle a markdown renderer or sanitizer (react-markdown is a safe default; it doesn't render raw HTML unless you explicitly enable a raw-HTML plugin, so keep it that way unless you fully trust the backend's output).

9. Handling errors

Drop <InsytfulSearch.ErrorCallout /> inside Portal and it appears automatically when a request fails (network error, non-2xx response, stream failure). It accepts title/text/cta props if you want to customise the copy or add a retry/contact-support action — otherwise it falls back to sensible defaults. No manual error-state wiring is required on your end.

10. Mode switching (optional)

Only relevant if you have a classic search fallback.

If you want to offer both an AI-answer mode and a traditional "go to search results page" mode, wrap your Input/Messages in Modes:

tsx
<InsytfulSearch.Modes defaultValue="ai">
  <InsytfulSearch.ModeSwitch />
  <InsytfulSearch.Mode value="ai">
    <InsytfulSearch.Input />
    <InsytfulSearch.Messages />
  </InsytfulSearch.Mode>
  <InsytfulSearch.Mode value="classic" path="/search" queryParam="q">
    <InsytfulSearch.Input />
  </InsytfulSearch.Mode>
</InsytfulSearch.Modes>

In classic mode, submitting navigates to path?queryParam=<query> instead of streaming an AI answer (with a same-origin check for safety). Skip this section entirely if you only want the AI experience.

11. Going beyond the built-in UI (optional)

If the compound components don't fit your design (e.g. you want the AI answer embedded inline on a page rather than in a modal), the package also exports the underlying RAG hooks directly, so you can build fully custom UI:

tsx
import { useRAGResponse } from "insytful-ai-search-components";

function InlineAnswer() {
  const { response, isLoading, error, sendMessage } = useRAGResponse({
    config: "your-config",
    baseUrl: "https://api.example.com",
  });

  return (
    <div>
      <button onClick={() => sendMessage("What is this product?")}>Ask</button>
      {isLoading && <p>Loading…</p>}
      {response && <p>{response}</p>}
      {error && <p>Something went wrong.</p>}
    </div>
  );
}

Only reach for this if the compound components genuinely don't fit — you take on your own loading/error/streaming UI in exchange for full layout control. For most integrations, InsytfulSearch.Root/Portal is the faster and better-supported path.

Still need help?

If you still need help after reading this article, don't hesitate to reach out to the Insytful community on Slack or raise a support ticket to get help from our team.
New support request