← all posts
how-to · fait maison

Embed the Krispy Chat Widget in Next.js, React, or HTML

Three copy-paste patterns to add a Krispy chat widget to any site — plain script tag, React useEffect hook, or Next.js Script component. No npm install.

Shai Snir
embednext.jsreactchat-widgethowto
Buttr the croissant mascot

🥐 Buttr: one script tag. that's the whole thing. sorry to disappoint anyone hoping for a saga.

Buttr the croissant reading the Next.js Script docs

Buttr pre-reading the Next.js docs so your first embed goes clean.

The Krispy chat widget is a single JavaScript file served from your Worker. A <script> tag pointing at that URL is all it takes — no npm install, no build step, no SDK. Three patterns cover every setup: plain <script> for HTML sites, useEffect for React SPAs, and Next.js's <Script> component for App or Pages Router.

What do you need before embedding the Krispy widget?

You need a live Krispy Worker URL — something like https://krispy.<your-subdomain>.workers.dev. If you haven't deployed one yet, the Cloudflare Workers self-hosting guide walks you through it in about five minutes. Cloudflare Workers' free tier covers up to 100,000 requests per day at no cost (Cloudflare Workers pricing), so a self-hosted Krispy instance costs nothing in hosting for most small and medium sites.

Not interested in managing infrastructure? Krispy Cloud is the hosted option — 14-day free trial, then $19/mo flat. Every embed pattern below works with a Cloud URL; just swap yours in.

How do you embed the Krispy chat widget in plain HTML?

For static sites, CMS pages, or any HTML file, paste this just before </body>:

<script
  src="https://krispy.<your-subdomain>.workers.dev/widget.js"
  async
></script>

The async attribute tells the browser to fetch the script in parallel with HTML parsing — no render-blocking (MDN: script async). The widget self-initializes on load; no configuration object required for basic use.

To set the opening greeting, widget position, or theme, add data-* attributes:

<script
  src="https://krispy.<your-subdomain>.workers.dev/widget.js"
  data-greeting="Got questions? Ask away."
  data-position="bottom-left"
  data-theme="dark"
  async
></script>

Everything else — the AI persona, escalation triggers, Telegram handoff — lives server-side in krispy.config.ts. The embed stays thin.

Buttr the croissant mascot

🥐 Buttr: five lines of HTML. your widget is live. i'm not sure what you were worried about.

How do you add the Krispy widget to a React or Vite app?

React has no built-in primitive for loading third-party scripts, so you append the element once via useEffect and clean up on unmount:

// components/KrispyWidget.tsx
import { useEffect } from "react";

export function KrispyWidget() {
  useEffect(() => {
    const script = document.createElement("script");
    script.src = "https://krispy.<your-subdomain>.workers.dev/widget.js";
    script.async = true;
    document.body.appendChild(script);
    return () => {
      document.body.removeChild(script);
    };
  }, []);

  return null;
}

Place it once at your app root, outside your router, so it persists across client-side navigation:

// App.tsx
import { KrispyWidget } from "./components/KrispyWidget";

export default function App() {
  return (
    <>
      <Router>{/* your routes */}</Router>
      <KrispyWidget />
    </>
  );
}

The empty [] dependency array loads the script exactly once. The cleanup handles the rare case where the whole app unmounts. The widget persists across route changes — the chat session survives navigation without re-mounting.

How do you embed the Krispy widget in a Next.js App Router project?

Use Next.js's <Script> component with strategy="afterInteractive". This defers the widget until after browser hydration, which avoids Lighthouse Total Blocking Time penalties and eliminates a whole class of hydration mismatch warnings (Next.js Script component docs):

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://krispy.<your-subdomain>.workers.dev/widget.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

The mistake that trips people up: putting a raw <script> tag inside Server Component JSX. Next.js renders it into the HTML output, but the browser may not execute it reliably and you'll get console warnings about unrecognized DOM properties. The <Script> wrapper from next/script handles deduplication, load timing, and hydration correctly. That's the entire fix.

Pages Router

Same idea in _app.tsx:

// pages/_app.tsx
import type { AppProps } from "next/app";
import Script from "next/script";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://krispy.<your-subdomain>.workers.dev/widget.js"
        strategy="afterInteractive"
      />
    </>
  );
}

What can you configure from the embed vs. the server?

Nearly all of Krispy's behavior lives server-side in krispy.config.ts — AI system prompt, escalation triggers, and bot-to-human handoff routing. The full installation walkthrough covers the server-side config end to end. From the embed itself, three cosmetic options are available:

AttributeValuesDefault
data-greetingAny stringKrispy's default opening line
data-position"bottom-right" / "bottom-left""bottom-right"
data-theme"light" / "dark""light"

These attributes work identically across all three embed patterns — pass them as data-* attributes on the <script> tag (plain HTML), as properties on the element in the useEffect (React), or as props on the <Script> component (Next.js).

Keeping logic server-side means you update the bot's persona or escalation rules without touching your frontend deployment.

Buttr the croissant mascot

🥐 Buttr: your frontend doesn't need to know your Telegram handle. that's between me and the config.

Pick the pattern that matches your stack: plain script tag for anything that isn't a JS framework, useEffect for React SPAs, <Script strategy="afterInteractive"> for Next.js. Same widget, same behavior — just how your framework picks up the script cleanly.

Want to run Krispy yourself? Star the repo on GitHub and follow the Cloudflare Workers self-hosting guide. Or skip the setup — Krispy Cloud handles it for you, 14-day free trial, $19/mo flat.

FAQ

How do I embed the Krispy widget in a Next.js App Router project?

Import Script from next/script and place it in your root app/layout.tsx with strategy="afterInteractive". Avoid putting a raw <script> tag inside Server Component JSX — Next.js renders it into HTML output, but the browser may not execute it reliably and you'll see hydration warnings. The next/script wrapper handles deduplication and post-hydration load timing. Pass your Worker URL as the src prop.

Can I embed the Krispy chat widget without installing anything from npm?

Yes. The widget is a standalone JavaScript file served directly from your Krispy Worker (or Krispy Cloud). No npm package, no bundler change, no build step needed. A single <script src="..." async> tag is the entire integration — the script self-initializes on load. The React and Next.js patterns use document.createElement or the <Script> component, both of which reference the same hosted file.

Does the Krispy widget work in a React app with client-side routing?

Yes. Load the script once in a useEffect with an empty dependency array [] at your app root, outside the router. The widget runs independently of React's router and persists across client-side navigation without re-mounting or losing the active conversation. Route changes don't restart the widget or drop the chat session.

Why does my Next.js app show hydration warnings when I add a raw script tag?

Raw <script> tags in Server Component JSX can cause hydration mismatches because Next.js renders them during SSR, but browser script execution timing doesn't align with React's hydration pass. Switch to import Script from "next/script" with strategy="afterInteractive" — it defers execution until after hydration and the warnings stop.

Where do I configure the AI persona and Telegram handoff — in the embed or on the server?

On the server, in krispy.config.ts on your Krispy Worker. The AI system prompt, escalation triggers, and Telegram handoff are all configured there. The embed's data-* attributes only control presentation — opening greeting text, widget position, and light/dark theme. Keeping logic server-side means you update bot behavior without redeploying your frontend.

What's the difference between the plain HTML, React, and Next.js embed patterns?

The widget file is identical in all three cases — it's a matter of how your environment loads a script tag cleanly. Plain HTML: async attribute on a <script> tag. React SPA: a useEffect that creates and appends the element once. Next.js: the <Script> component from next/script with strategy="afterInteractive". Pick the pattern that fits your stack; the widget behaves the same in all three.

Buttr the croissant mascot

that's the whole thing. want me to answer your visitors like this? i self-host in one command. 🥐