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 sites — plus how to restrict it to your domains, control the launcher, and test it.

Short answer
To add an AI chat widget to a website, paste one script tag before the closing </body> tag. The tag loads a small launcher, and the chat panel itself only loads when a visitor clicks it, so page speed is unaffected. The same tag works on plain HTML, WordPress, Shopify, Webflow, Squarespace and Wix; React and Next.js sites use a component instead. Restrict the widget to your own domains afterwards so the snippet cannot be reused elsewhere.
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 about 6 KB. 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. Nothing else exists on the page until someone clicks it.
- The chat panel is an iframe pointing at
widget.pepochat.com. It is created on the first click, not on page load. That is why the widget has no effect on your page's load time or Core Web Vitals: until a visitor opens it, the only cost is the loader.
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 Project 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 is available on paid site plans. On the free Starter plan you can test the widget with a Code embed element placed on one page instead.
Squarespace and Wix
Squarespace: Settings → Advanced → Code Injection, paste into Footer, save. Code injection requires a Business plan or higher.
Wix: Settings → Custom code (under Advanced), click Add custom code, paste the snippet, choose All pages and Body — end, then apply. Custom code needs a premium plan with a connected domain.
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 looks itself up by its data-organization-id attribute rather than relying on document.currentScript, which is what makes it work through next/script (and through tag managers, where currentScript is null). Not every widget does this; if you install a different vendor's tag through a framework and the launcher never appears, that 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.
- Paste the snippet exactly as in the HTML section.
- Set the trigger to All Pages and publish the container.
Because GTM injects scripts dynamically, it is another case where a loader that depends on document.currentScript fails; the PepoChat loader handles it for the reason described above.
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.
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 about 6 KB; the iframe is created on first click. On a typical site the measurable impact on Largest Contentful Paint and Interaction to Next Paint is zero, because nothing runs during the load phase except a tiny script that draws one button.
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 tracking cookies on your domain. Visitor identity lives in the iframe's own storage and is anonymous until the visitor chooses to verify an email. Most consent tools therefore don't need to gate it, but if your policy blocks all third-party scripts until consent, load the snippet from the consent tool's "after acceptance" hook.
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 |
| "We couldn't reach the chat service" | Network blocked by 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 |
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?
- A well-built widget should not. PepoChat's loader is about 6 KB, loads asynchronously, and does not create the chat iframe until a visitor clicks the launcher, so it has no measurable effect on Core Web Vitals.
- 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.
Try this on your own site in ten minutes
PepoChat includes every feature on the free plan — 500 AI replies and 10 knowledge sources a month, no credit card.