How to embed an AI chatbot on a React website
What you get
The AskVault widget is a vanilla JavaScript embed that renders inside any React app without needing a React-specific package. There's no npm install, no peer-dependency conflicts, and no risk of pulling in a second copy of React. The widget creates its own root element, mounts an isolated Preact runtime inside it, and never touches your React tree.
That's deliberate. We tried shipping an @askvault/react package and the bug reports were 90% version-mismatch issues with hosts running older React or pinning specific Next.js builds. A plain script tag works on every React version from 16 to 19, every Next.js version from 12 to 15, every Vite build, every Remix app, and every Astro island.
Three ways to install it
You have three options depending on how you want the widget loaded. Pick one.
Option 1: drop it in the root HTML (simplest)
For Create React App, Vite, or any setup with a single index.html template, paste the AskVault script tag right before the closing </body> tag:
<!-- public/index.html (CRA) or index.html (Vite) --><!DOCTYPE html><html> <head>...</head> <body> <div id="root"></div>
<script async defer src="https://api.askvault.co/widget.js" data-workspace-token="wt_yourtoken_here" ></script> </body></html>That's it. Restart your dev server, reload, and the launcher button appears bottom-right.
Option 2: load it from _app.tsx or app/layout.tsx (Next.js)
For Next.js, you can't edit the document head directly in most setups. Use the built-in next/script component to load the widget once per session:
// app/layout.tsx (Next.js 13+ App Router)import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://api.askvault.co/widget.js" strategy="afterInteractive" data-workspace-token="wt_yourtoken_here" /> </body> </html> );}For the Pages Router, use pages/_app.tsx and the same Script component. The afterInteractive strategy is correct here. Don't use beforeInteractive because the widget never needs to run before hydration.
Option 3: a reusable component (advanced)
If you want to conditionally mount the widget (only on certain routes, only for logged-in users, only after a delay) wrap it in a one-off component:
import { useEffect } from 'react';
export function AskVaultWidget({ token }: { token: string }) { useEffect(() => { if (document.getElementById('askvault-widget-script')) return; const s = document.createElement('script'); s.id = 'askvault-widget-script'; s.src = 'https://api.askvault.co/widget.js'; s.async = true; s.defer = true; s.setAttribute('data-workspace-token', token); document.body.appendChild(s); }, [token]); return null;}Then import it where you want:
import { AskVaultWidget } from '@/components/AskVaultWidget';
export default function SupportPage() { return ( <> <h1>Help & support</h1> <AskVaultWidget token={process.env.NEXT_PUBLIC_ASKVAULT_TOKEN!} /> </> );}This pattern is useful for a logged-in-only support widget, a route-specific widget, or A/B testing whether to show the widget at all.
Pass authenticated user context
If you want the widget to know who the user is (so the agent can answer "what's the status of my order?" without a separate ID lookup), use the identify API after the widget loads:
useEffect(() => { const id = setInterval(() => { if (window.AskVault) { window.AskVault.identify({ user_id: currentUser.id, email: currentUser.email, name: currentUser.name, // Optional: HMAC-signed verification token to prevent impersonation verification_token: currentUser.askvaultVerificationToken, }); clearInterval(id); } }, 100); return () => clearInterval(id);}, [currentUser]);The verification_token is generated server-side using your workspace's HMAC secret (visible in Settings > Identity Verification). Without it, anyone in the browser DevTools could call identify with a fake user ID. We strongly recommend enabling identity verification before passing user context. Growth+
Hide the widget on certain pages
Sometimes you don't want the launcher on every page (checkout, in-product flows, embedded iframes). Hide it conditionally with the show and hide methods:
useEffect(() => { if (window.AskVault) { window.AskVault.hide(); return () => window.AskVault.show(); }}, []);A useEffect like this in your checkout layout cleanly hides the widget while users are in flow and brings it back when they leave.
Verify the widget loads
After deploying, three quick checks confirm the widget is healthy:
- The launcher renders on every page. Visit your homepage, a product page, and a logged-in route. The widget should appear on all three. If it's missing on one, that route isn't loading the script.
- Answers cite your indexed content. Ask a question only your knowledge base can answer. The response should include a "Sources" link. If it doesn't, indexing probably hasn't completed yet. Check Knowledge Hub in the dashboard.
- Conversations show up in the dashboard. Send a test message. Within 30 seconds it should appear under Live Chat > Conversations. If it doesn't, your workspace token is wrong.
Bundle size and performance
The widget bundle is 12 KB gzipped. It loads with async + defer, so it never blocks the parser. On a typical 4G mobile connection the launcher button paints within 250 ms of DOMContentLoaded. Lighthouse Performance impact: zero.
The widget does not fetch chat history, conversation state, or any backend data until the launcher is clicked. That means it's invisible to your First Contentful Paint and Largest Contentful Paint metrics. Both Core Web Vitals signals stay clean even on a marketing landing page with strict performance budgets.
TypeScript types
If you're using TypeScript, declare the global so you get autocomplete on window.AskVault:
declare global { interface Window { AskVault?: { identify: (user: { user_id: string; email?: string; name?: string; verification_token?: string }) => void; show: () => void; hide: () => void; open: () => void; close: () => void; }; }}export {};The AskVault object is available about 250 ms after page load. If you need it earlier (during hydration), poll for it like the useEffect example above shows.
Troubleshooting
A few real failure modes and what to check:
- Widget never appears. Check the Network tab for a request to
widget.js. If it 404s, your token is malformed. If it loads but the launcher is missing, the workspace was deleted or disabled in your dashboard. - Widget appears but answers nothing. Indexing hasn't completed. Open Knowledge Hub and wait for the progress bar to hit 100%.
- CORS error on production. Add your production domain to Settings > Widget > Allowed Origins. The widget enforces same-origin rate limiting by default.
- Conflict with another chat widget. Both widgets render in fixed-position containers and overlap. Either hide the other widget on relevant routes, or move the AskVault widget with Settings > Widget > Position > Bottom-left.
- Build-time error "window is not defined". You're trying to access
window.AskVaultduring SSR. Wrap the access in auseEffecthook (client-only) or checktypeof window !== 'undefined'first.
FAQ
Does the widget work with React Server Components?
Yes. The script tag goes in the root layout (a Server Component), but the widget itself runs entirely client-side after hydration. You don't need to mark anything 'use client' unless you want to interact with window.AskVault from a component.
Can I lazy-load the widget?
Yes. Use the useEffect pattern (Option 3) and attach the script only when the user scrolls past a fold, hovers a help icon, or whatever signal you want. The widget bundle is small enough that lazy-loading usually isn't worth the complexity, but it's supported.
Will the widget bloat my JavaScript bundle?
No. It's loaded over the network as a separate script. Your app's bundle stays the same size whether you embed the widget or not.
Can I run multiple widgets on the same page?
No. One workspace, one widget. If you need different bots for different sections of your app (e.g., a sales bot on marketing pages and a support bot on logged-in routes), create separate workspaces and conditionally render whichever script tag is right for the route.
How do I test locally without polluting analytics?
Add data-sandbox="true" to the script tag during development. The widget runs in sandbox mode: messages don't count against your quota and conversations don't appear in the dashboard.
Related guides
- Install the AskVault widget on any website
- How to embed an AI chatbot on WordPress
- How to embed an AI chatbot on Shopify
- How to restrict the AI bot to specific URLs only
- Identity verification with HMAC
- Widget customization in Widget Studio