How to Add an AI Chat Widget to Any Website With One Script Tag
Copy-paste install guide for an AI chat widget on HTML, WordPress, Shopify, Webflow, React and Next.js, plus domain restriction, launcher control and testing.

Short answer
To add an AI chat widget to a website, paste one script tag before the closing </body> tag. The tag loads a 3.2 KB launcher after your content and fetches the chat panel in idle time, so it never delays your page. It works on plain HTML, WordPress, Shopify, Webflow, Squarespace and Wix; React and Next.js sites use a component. Then restrict the widget to your own domains so nobody can reuse the snippet.
Installing a chat widget is the easiest part of setting up AI support, and yet it is where most "it doesn't show up" tickets come from, because every website builder hides the footer in a different place. This guide gives you the exact snippet and the exact place to paste it for each platform, then covers the things people ask after the widget appears: how to position it, how to open it from your own button, how to stop it running on domains you don't own, and how to make sure it never slows your site down.
The examples use PepoChat, whose loader is a single script tag. If you use a different product the mechanics are the same; only the URL and the attribute names change.
What the script tag actually does
Understanding the three parts of a widget makes every platform-specific step obvious.
- The loader is the script you paste. PepoChat's is 7.9 KB, or 3.2 KB compressed. It runs after your page has rendered, reads your organization id from its own
data-organization-idattribute, and draws a launcher button in the corner. - The launcher is a fixed-position button in the corner of the page.
- The chat panel is an iframe pointing at
widget.pepochat.com. By default it is fetched in browser idle time after your page has finished loading, about 523 KB over the network, so the first click opens instantly; withdata-preload="false"it is created on the first click instead and the page pays only for the loader. When we measured 13 chat widgets on a blank page, neither mode changed the page's Largest Contentful Paint or its Lighthouse score.
The iframe is also why the widget cannot read or change anything on your page. Your site and the chat run in separate browsing contexts and only talk through explicit messages (open, close, resize).

Before you start: get your organization id
Every snippet below contains an organization id. In PepoChat, open Widget Setup in the dashboard sidebar; it shows ready-made snippets for HTML, JavaScript, React and Next.js with your id already filled in. Copy from there rather than typing the id by hand. If you don't have an account yet, start a free workspace first; the widget works on the free plan with every feature enabled.
Throughout this article YOUR_ORGANIZATION_ID stands for that value.
Plain HTML websites
Paste this immediately before </body>:
<!-- Paste before the closing </body> tag -->
<script
src="https://widget.pepochat.com/widget.js"
data-organization-id="YOUR_ORGANIZATION_ID"
></script>
Optional attribute: data-position="bottom-left" moves the launcher to the left corner. The default is bottom-right.
Why before </body> and not in <head>? A script in the head runs before the body exists, so a widget that appends its launcher to document.body would have nothing to append to. Modern loaders wait for the DOM, so head placement usually works, but the footer is the safe choice and it never blocks rendering. If your template only lets you edit the head, add the defer attribute and it will behave the same.
Save, reload, and the launcher appears in the corner. That's the whole install for a static site.
WordPress
There are three common ways, in order of preference.
A footer code option in your theme. Many themes (Astra, GeneratePress, Kadence and others) have a "Footer scripts" or "Custom code" field under Appearance → Customize. Paste the snippet there. It survives theme updates.
A block theme's footer template part. In Appearance → Editor, open the Footer template part, add a Custom HTML block at the end, and paste the snippet. Save the template.
The theme's footer.php. In Appearance → Theme File Editor (or over SFTP), find footer.php and paste the snippet just above <?php wp_footer(); ?> or above </body>. Do this in a child theme, otherwise the next theme update erases it. WordPress documents the theme file structure if you need to find the right file.
You can also use a code-snippet plugin; they all offer a "footer" location. Avoid pasting the tag into a post or page body: the block editor will strip or escape it.
Shopify
Shopify themes have one layout file that wraps every page:
- In the admin, go to Online Store → Themes, click the three dots next to your live theme, then Edit code.
- Open
layout/theme.liquid. - Paste the snippet just above
</body>and save.
That covers every storefront page. The checkout is separate: on Shopify Plus you can add it to checkout.liquid where that file is still available; on standard plans the checkout does not accept custom scripts. Shopify's theme code editing docs cover the editor itself.
Tip for stores: once the widget is live, connect your store so the assistant can answer "where is my order" with real data. The use cases page shows the ecommerce setup.
Webflow
Webflow has a site-wide custom code panel:
- Open Site settings → Custom code.
- Paste the snippet into Footer code.
- Save and Publish the site. Custom code only goes live on publish, which is the step most people miss.
Custom code needs a paid Site plan or a paid Workspace plan; the free Starter plan cannot run it, and the Code Embed element follows the same rule. The Webflow guide covers staging domains, page-level code and Framer, Carrd and Ghost.
Squarespace and Wix
Squarespace: Pages → Custom Code → Code Injection (on version 7.0 sites, Website Tools → Code Injection), paste into Footer, save. Code injection needs a Core, Plus or Advanced plan, or a legacy Business or Commerce plan; the Squarespace guide walks through it step by step.
Wix: Settings → Custom Code (under Development & integrations), click Add Custom Code, paste the snippet, choose All pages, Load code once and Body - end, then apply. Custom code needs a Premium plan with a connected domain; the Wix guide also compares Wix's own Smart Chat.
React and single-page apps
A single-page app has no </body> you write by hand, so the loader is added from a component. Render it once, near the root, and it stays for the life of the app:
// Render <PepoChatWidget /> once, near the root of your app
import { useEffect } from "react";
export function PepoChatWidget() {
useEffect(() => {
// The script only boots once per page load — on a remount (e.g. after a
// route change) bring the launcher back through its API instead.
if (window.EchoWidget) {
window.EchoWidget.init({ organizationId: "YOUR_ORGANIZATION_ID" });
} else {
const script = document.createElement("script");
script.src = "https://widget.pepochat.com/widget.js";
script.async = true;
script.setAttribute("data-organization-id", "YOUR_ORGANIZATION_ID");
document.body.appendChild(script);
}
return () => {
window.EchoWidget?.destroy?.();
};
}, []);
return null;
}
Two notes. The cleanup function removes the launcher when the component unmounts, which lets you show the widget on some routes and not others. And because React's development mode mounts components twice, you may briefly see a console warning that the widget is already initialised; the loader ignores the duplicate, and it does not happen in production builds.
Next.js
Use the built-in Script component in your root layout, inside the <body>:
// app/layout.tsx — inside <body>
import Script from "next/script";
<Script
src="https://widget.pepochat.com/widget.js"
strategy="lazyOnload"
data-organization-id="YOUR_ORGANIZATION_ID"
/>
lazyOnload waits until the browser is idle, which is the right trade-off for a support widget. Use afterInteractive if you want the launcher to appear as soon as the page is usable.
The PepoChat loader reads its settings from its own script tag, and when document.currentScript is not available it falls back to the first script on the page whose address contains widget and that carries a data-organization-id. next/script keeps data attributes, so the fallback finds them there. Google Tag Manager does not, which is why GTM needs the JavaScript snippet below. If you install a different vendor's tag through a framework and the launcher never appears, a lost configuration attribute is the usual cause.
Google Tag Manager
If marketing controls the site through GTM, the install can live there:
- Create a new tag of type Custom HTML.
- In PepoChat's Widget Setup, copy the JavaScript snippet, not the HTML one, and paste it between
<script>and</script>tags. - Set the trigger to All Pages, check it in Preview, then publish the container.
GTM re-creates the script tags in a Custom HTML tag with their address and a few standard attributes only, so the HTML snippet's data-organization-id is dropped and the console shows "data-organization-id attribute is required". The JavaScript snippet creates the loader and sets that attribute in code, so it survives. Our Google Tag Manager guide covers triggers, consent settings and when not to use GTM.
Position, launcher and opening it from your own button

Corner. data-position="bottom-left" or "bottom-right" on the script tag.
Colours, greeting and suggested questions are set in the dashboard under Widget Customization, not in the snippet, so you can change them without touching the site. The widget picks them up on the next page load.
Your own trigger. The loader exposes a small API on window.EchoWidget:
EchoWidget.show(); // open the panel
EchoWidget.hide(); // close it
EchoWidget.destroy(); // remove the launcher and panel entirely
EchoWidget.init({ organizationId: "…", position: "bottom-left" }); // re-create
So a "Chat with us" link anywhere on the page is just onclick="EchoWidget.show()". init() is also how a single-page app brings the launcher back after destroy().
Mobile. The launcher stays fixed in its corner, and the panel is capped to the viewport (100vw wide, 100vh tall minus the launcher) on small screens. There is nothing to configure.
Should the widget load on every page?
Usually yes. Visitors ask questions from product pages, pricing pages and blog posts far more often than from a dedicated contact page, and a launcher that only appears in one place is a launcher most people never see. The exceptions are checkout flows, where a floating button can cover a "Place order" control on small screens, and embedded or full-screen tools where the corner is already in use. On those pages either leave the snippet out or call EchoWidget.destroy() after load; hide() only closes the chat window and keeps the launcher. If you use a tag manager, a trigger that excludes /checkout paths does the same job without touching the site. Whatever you choose, keep the organization id identical everywhere: it is what ties every conversation back to one workspace and one knowledge base.
Restrict the widget to your own domains
A script tag is public by definition: anyone can view source, copy it, and paste it on their site. With most widgets that would let a stranger run chats, and spend your AI replies, from a page you don't control.
The fix is an allow-list. In PepoChat's Widget Customization settings, add the domains the widget may run on, for example example.com and www.example.com. The widget reports the page origin when a visitor starts a session, and the backend refuses to create a session from an origin that is not on the list. A copied snippet then draws a launcher that never connects.
Add every hostname you actually use, including staging and localhost if you test locally, and leave the list empty only while you are setting things up.
Performance and Content Security Policy
Load time. The loader is async and 8.2 KB (3.4 KB compressed, measured 22 October 2026), and it draws one button after your content. By default it then fetches the chat frame, about 523 KB, in idle time after the page has loaded; with data-preload="false" the frame waits for the first click. On our blank-page test neither mode changed Largest Contentful Paint or the Lighthouse score, and with Chrome's site isolation switched off, as on many Android phones, the preloaded frame added 37 ms of Total Blocking Time.
CSP. If your site sends a Content-Security-Policy header, allow the widget origin in two directives:
script-src 'self' https://widget.pepochat.com;
frame-src https://widget.pepochat.com;
The chat's own network requests happen inside the iframe under the widget's origin, so you do not need to add anything to connect-src. MDN has a thorough reference for CSP directives if you are writing a policy from scratch.
Consent banners. The widget sets no cookies on your domain. Visitor identity lives in the iframe's own storage, written when a visitor first opens the chat, and is anonymous until the visitor chooses to verify an email. Whether that counts as strictly necessary is your call: our guide to loading the widget after consent measures what chat widgets store before a click and gives a click-to-load pattern, and data-preload="false" keeps the chat frame unloaded until the launcher is clicked.
Test it before you announce it
Five checks, two minutes:
- The launcher appears on the home page, a deep page, and on a phone.
- Ask a question that your knowledge base answers. If you haven't built one yet, do that next: how to train an AI chatbot on your website and PDFs.
- Ask something it can't know and confirm it says so and offers a human.
- Ask for a human and confirm the conversation shows up in your team inbox.
- Open the site from a domain not on your allow-list (or a local copy of the page) and confirm the widget refuses to start a session.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No launcher at all | Snippet not published (Webflow, Wix) or pasted into a page body that escaped it | Publish the site; move the tag to the footer/custom-code area |
| Launcher appears, panel says the organization is not valid | Wrong or truncated data-organization-id | Re-copy the snippet from Widget Setup |
| Works on desktop, hidden on mobile | A cookie banner or sticky footer with a higher z-index | Lower the banner's z-index or move it; the launcher uses a very high value already |
| "The chat couldn't load" (after 15 seconds) or "The chat is taking too long to load" | The chat frame never arrived: a network problem, an ad blocker, or a CSP without frame-src | Add the CSP directives above; test in a clean browser profile |
| Widget refuses to start a session | Page origin not on the allowed-domains list | Add the hostname (including www.) in Widget Customization |
| Two launchers | Snippet pasted twice (theme + plugin, or GTM + footer) | Remove one; the loader ignores a duplicate tag but a second loader from another vendor won't |
| Launcher disappears after navigating in a React app | Component unmounted and destroy() ran | Keep the component mounted at the root, or call EchoWidget.init() on remount as in the React snippet |
If the launcher still does not appear, work through the 12 causes of a chat widget not showing, each with a one-minute check.
What to do next
With the widget live, the two things that decide whether customers use it are the content it answers from and the handoff when it can't. Build the knowledge base with the training guide, set the greeting and suggested questions in Widget Customization, and watch the first day's conversations in the inbox. Everything in this article is available on PepoChat's free plan; the pricing page lists the monthly limits, and the contact page is there if you'd like a hand with an unusual setup.
Frequently asked questions
- Where should I put the chat widget script tag?
- Just before the closing </body> tag. That way it loads after your page content and never blocks rendering. If you use a tag manager or a framework component, any placement that runs after the page has a <body> works too.
- Will a chat widget slow down my website?
- Not measurably, if it loads after your content. PepoChat's loader is 7.9 KB (3.2 KB compressed) and asynchronous. By default it fetches the chat frame, about 523 KB, in idle time after the page has loaded; with data-preload set to false it waits for the first click. In our October 2026 blank-page test, neither mode changed Largest Contentful Paint or the Lighthouse score.
- Can I add the widget to WordPress, Shopify or Webflow without a plugin?
- Yes. All three let you paste a script tag into a footer or custom-code setting: WordPress via a footer-code option or the theme's footer.php, Shopify via theme.liquid, and Webflow via Project settings → Custom code → Footer code.
- How do I stop someone copying my widget onto their site?
- Set the list of allowed domains in the widget settings. The backend then refuses to start a chat session from any page whose origin is not on the list, so a copied snippet does nothing elsewhere.
- Does the chat widget work on mobile?
- Yes. The launcher stays fixed in the corner you choose, the panel is capped to the viewport on small screens, and the same install works for responsive sites and progressive web apps.
- Can I open the chat from my own button?
- Yes. The loader exposes window.EchoWidget with show(), hide(), destroy() and init(). Call EchoWidget.show() from any button's click handler to open the panel; if the frame was not preloaded, show() loads it first. hide() closes the window but keeps the launcher and the conversation, so a Chat with us link in your menu works on every page.
Try this on your own site in ten minutes
PepoChat includes every feature on the free plan — 200 AI replies and 5 knowledge sources a month, no credit card.
Keep reading
Chatbase vs PepoChat: Which AI Support Agent Fits Your Team in 2026?
An honest side-by-side of Chatbase and PepoChat on pricing, free plan or trial, sources, handoff, booking, actions, channels and voice, and a pick by team type.
Intercom Fin Pricing Explained, and What a Flat-Plan Alternative Costs
What Intercom Fin's $0.99 per outcome adds up to once you count resolutions and seats, with a worked 2,000-conversation example and when a flat plan wins.
