AI Search React/TSX Implementation
Log in to add to favouritesPage 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
npm install insytful-ai-search-components
Peer dependencies — make sure your project already has these:
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
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
| Component | Role |
|---|---|
Root | Owns state and config. Everything else must be a descendant. |
Trigger | A button that opens the dialog. Renders wherever you place it — doesn't have to be near Portal. |
Portal | Boundary into the Shadow DOM. Everything inside it is the actual dialog content. |
Title / Description | Accessible heading/description, wired to aria-labelledby/aria-describedby automatically. |
Input | The question textarea + send button. |
Messages | Renders the streaming conversation transcript. |
ErrorCallout | Shown automatically when a request fails; customisable content. |
Suggestions | Optional clickable starter prompts. |
Disclaimer | Optional static text under the input. |
Close | Optional 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:
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/onOpenChangeentirely and the component manages open/closed internally viaTrigger. - Control it externally — pass
open/onOpenChangeif 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):
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:
<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):
<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:
- CSS custom properties — set on your app's
:root(they pierce the Shadow DOM automatically since custom properties inherit across shadow boundaries): themeprop — 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 throughtheme.
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:
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:
<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:
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.