React · Next.js · Vite

A support 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.

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>
  );
}

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.

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

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