The widget script is a plain, framework-agnostic <script> tag, so it works in a React, Vue, Angular, or any other single-page app the same way it works on a static site — you just need to load it once, not on every route change.
Load it once, in your app shell
- Complete the setup in “Add the chat widget to your website” first — enable the widget, add your origin(s) under Allowed website origins, and copy the embed snippet. This article only covers where to put that snippet in a single-page app.
- Add the
<script async src="https://your-canonical-host/widget.js"></script>tag to your app’s persistent HTML shell — the rootindex.html, or the equivalent layout/document component in frameworks like Next.js, Nuxt, or SvelteKit — not inside an individual route or page component. - Don’t add it again inside a route component “just in case.” The script guards itself against double-loading (see below), but loading it once in the shell is simpler and is all it needs.
Why you don’t need to reload it on navigation
The widget renders its button and iframe directly onto document.body, entirely outside your framework’s root or mount element. Client-side routers only ever touch what’s inside their own mounted root, so they never remove or remount the widget when the URL changes — once it’s loaded, it stays visible and functional across every client-side navigation without any extra code.
If you inject the script dynamically instead
Some single-page apps load third-party scripts programmatically — only after a route mounts, or gated behind a feature flag — rather than putting a static tag in the HTML shell. That works too: create a script element, set its src to the widget URL, and append it to the document; the widget still resolves its own origin correctly at that point. The script sets an internal flag the first time it runs and exits immediately on any later run, so it’s safe even if your app injects it more than once — for example once from server-rendered HTML and again during client hydration — you’ll never end up with two buttons.
Identity and context are not passed through yet
There’s currently no supported way to pre-fill or pass a signed-in visitor’s name, email, or any app context into the widget’s form. Every submission uses the same anonymous, verify-first flow described in “Add the chat widget to your website”: the visitor types their own name and email into the iframe form and has to click an emailed confirmation link before a ticket is created, even if your app already knows who they are. If you need to skip that confirmation step or attach account context automatically, use your workspace’s API, an inbound-email address, or a signed-in customer session in the portal instead of the embedded widget for that flow.
Origins still matter
The Allowed website origins list under Settings → Channels → Widget is unaffected by how you load the script — it’s still checked by exact origin whenever the iframe tries to open. Add every origin your single-page app is actually served from, including a staging or preview domain if you test the widget there, as separate entries.
Related: Add the chat widget to your website and Configure the chat widget.