React · Next.js · Vite

A React chatbot widget with no dependency to add

One script tag, or a fifteen-line component when you need conditions. Nothing enters your bundle, nothing conflicts with your React version, and removing it is deleting a file. If what you actually want is a chat UI library to wire to your own backend, the next section says so and names the good ones.

index.htmlhtml
<script src="https://customerbot.co/script.js" data-bot-id="YOUR_BOT_ID"></script>
app/layout.jsxjsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}

        <Script
          src="https://customerbot.co/script.js"
          data-bot-id="YOUR_BOT_ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Two things are called a React chatbot widget

They solve opposite problems and most search results for this phrase are the other one. Thirty seconds now saves you a wasted afternoon.

Not this

A chat UI component library

You npm install a React component, style it, and wire it to a backend you write — your own endpoint, an LLM API, whatever. You own the conversation logic and the retrieval.

You want React ChatBotify, react-chatbot-kit, chatbot-widget-ui or a build-it-yourself tutorial. Right answer when the chat is your product, or the behaviour is genuinely bespoke.

This page

A hosted assistant you drop in

The widget, the retrieval, the model routing, the transcripts and the handover already exist. You add one script tag and train it on your own docs.

This is us. Right answer when support is not the thing you are trying to build, and you would rather ship a feature than maintain a RAG pipeline.

Three ways in, and when each is right

Vite or Create React App

The simplest thing that works is the tag in index.html, above the closing body tag. No component, no effect, no lifecycle to reason about — the widget is outside React’s tree either way.

Use when the assistant should be on every page for everyone.

A component with useEffect

Inject the tag from a mounted component when you need conditions — signed-in users only, a specific route, a feature flag. The guard against a duplicate tag is the only subtle part.

Use when loading is conditional.

Next.js with next/script

App Router: drop <Script> into app/layout.jsx after {children}. Pages Router: pages/_app.jsx. Keep strategy="afterInteractive" so it loads once the page is usable.

Use in any Next.js app, server components included.

The component, in full

This is the whole integration for a React app that needs conditional loading. Copy it into your codebase — there is nothing to install and nothing to keep up to date.

Render <CustomerBot /> once, near the root. The guard is what keeps strict mode from giving you two of everything.

src/CustomerBot.jsxjsx
import { useEffect } from 'react';

const BOT_ID = 'YOUR_BOT_ID';

export function CustomerBot() {
  useEffect(() => {
    // Strict mode mounts effects twice in development, and a route change can
    // remount this component. The guard keeps one tag on the page, ever.
    if (document.querySelector(`script[data-bot-id="${BOT_ID}"]`)) return;

    const script = document.createElement('script');
    script.src = 'https://customerbot.co/script.js';
    script.async = true;
    script.dataset.botId = BOT_ID;
    document.body.appendChild(script);
  }, []);

  return null;
}
src/AppShell.jsxjsx
function AppShell() {
  const { user, isLoading } = useAuth();

  return (
    <>
      <Routes />
      {/* Only signed-in users get the assistant. Unmounting does not remove
          an already-injected widget, so gate on first render, not on toggle. */}
      {!isLoading && user && <CustomerBot />}
    </>
  );
}

Four things that bite

None of these are exotic. All four account for nearly every “the widget is not showing” report from a React codebase.

01

The double mount in strict mode

React 18 runs effects twice in development. Without a guard you get two script tags and two launchers, which then look like a widget bug rather than a mounting bug. Query for the existing tag by data-bot-id before appending.

02

Client-side navigation

The widget lives in document.body, not in your component tree, so a route change does not disturb it. That is what you want — a chat that resets on every navigation is worse than no chat. It also means unmounting the component does not remove the widget.

03

Content Security Policy

If you send a CSP, the script host has to be allowed in script-src, and the widget’s network calls in connect-src. A silent CSP block is the single most common reason a correctly placed tag does nothing; check the console before anything else.

04

Server-side rendering

Nothing here runs on the server. next/script handles the client boundary itself, and the useEffect version never executes during SSR — so no document is touched where document does not exist.

Where it goes, by framework

Same tag every time. Only the file changes.

App Router, site-wide

Put next/script in the root layout with strategy="afterInteractive". Server components are fine — the tag is rendered, not executed on the server.

App Router, some routes only

Put it in the layout of the route group that needs it, rather than branching on pathname in a client component. Less state, fewer surprises.

Pages Router

next/script in _app.js gives you the same thing. Adding it to _document.js also works but you lose the loading-strategy control.

Vite, CRA, Remix, Astro

The plain script tag in index.html is the whole integration. No framework-specific wrapper is needed or provided.

If you send a Content-Security-Policy

Allow the widget’s host in script-src and its API calls in connect-src. A blocked script fails quietly except for one console line, which is why this is worth checking before you start rereading your JSX.

Content-Security-Policy:
  script-src 'self' https://customerbot.co;
  connect-src 'self' https://customerbot.co;

Developer questions

What is a React chatbot widget?

The phrase covers two different things. One is a UI component library — you install a React package, style the chat interface and connect it to a backend you build yourself, owning the conversation logic and any retrieval. The other is a hosted assistant that already handles all of that and drops into your app as a script tag or a small component. Both are legitimate; they suit opposite situations. This page is the second kind, and the section above names good options for the first.

Does it work with the Next.js App Router?

Yes. Use next/script with strategy="afterInteractive" in your root layout for a site-wide widget, or in the layout of a route group when only part of the app needs it. Server components are not a problem — the tag is rendered on the server and executed in the browser, so nothing touches document where document does not exist.

Does it work with React 19?

Yes, and with 17 and 18. Because nothing is installed there is no peer-dependency range to satisfy and no version to keep in step with your app — which is the main practical argument for a script tag over a package.

What does it cost in bundle size?

Nothing in your bundle. The widget is fetched separately from a CDN after your app is interactive, so it does not enter your build output, does not affect your chunk sizes and should not move your Largest Contentful Paint.

Is there an npm package?

No, and deliberately so. The integration is a script tag, which means no dependency to version, no peer-dependency conflict with your React version, and no bundle-size cost in your app. The component on this page is the whole “package” — copy it into your codebase and it is yours.

Does it work with TypeScript?

Yes. The component is plain DOM work; rename it to .tsx and the only annotation you need is none. script.dataset.botId is typed as a string on HTMLScriptElement already.

Can I load it only for signed-in users?

Yes — render the component behind your auth check. Be aware that once the script has injected the widget, unmounting the component does not remove it, so gate on the first render rather than toggling it during a session.

Does it work in React Native?

No. This is a web widget that mounts DOM nodes. For mobile, CustomerBot ships its own Android and iOS companion app for managing conversations, and your users can reach the assistant through your web views.

Will it break my hydration?

No. The widget mounts outside your React root after hydration, so React never sees its nodes and there is nothing to mismatch.

Can I pass user context into the conversation?

The tag itself takes the bot ID. For answers that depend on a specific user or account, the right mechanism is an API tool: describe your endpoint in plain English, CustomerBot generates the tool schema, and the assistant calls it during the conversation.

Fifteen lines and a bot ID

Point it at your docs, copy the component, and have it answering inside your app this afternoon.

Start free
Ask CustomerBotAnswers from our product docs