Quick start
#Atria is one classic script tag. It adds a support launcher to the corner of your page and opens the chat in an isolated iframe.
In the Atria admin, open Agents, pick an agent and copy its snippet. It already contains your agent ID (it starts with
agt_).Paste it right before the closing
</body>tag of every page that should show the chat. Adeferscript in<head>works too.index.html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>Prefer attributes over query strings?
data-agent-idworks the same way.html <script src="https://atria.bemagic.online/widget.js" data-agent-id="agt_YOUR_AGENT_ID" defer></script>Reload your site and click the launcher. Messages show up live in Admin → Sessions.
How the widget works
#Built to be a polite guest on any page: tiny, isolated, and lazy.
The loader is about 3.5 kB gzipped, with no dependencies. It renders the launcher inside its own shadow root (
#atria-support), so your CSS can't break it and its CSS can't leak into your page.The chat itself is an iframe served from
https://atria.bemagic.online, created only when a visitor opens the launcher. Nothing heavy loads until then.All network requests happen inside the iframe. Your page only needs to allow loading the script and framing the chat.
The visitor gets an anonymous ID stored in your site's
localStorage(atria:visitor-id). The open conversation and whether the chat is open are kept insessionStorage, so the chat follows visitors across pages.When the chat is minimized, new replies show up as an unread badge on the launcher.
Identify signed-in visitors
#Tell Atria who is chatting, so agents and operators greet people by name and see useful context.
Set
window.AtriaSettingsanywhere on the page. It's read when the visitor opens the chat, so you can set it after sign-in without reloading the widget.html <script> window.AtriaSettings = { visitor: { id: "user_8231", // your user ID (string or number) name: "Marta Silva", email: "marta@example.com", metadata: { plan: "Pro", company: "Crumbs Bakery" }, }, }; </script>
Content Security Policy
#If your site sends a CSP header, allow Atria in two directives. That's all.
Add Atria's origin to
script-src(to load the loader) andframe-src(to show the chat). Noconnect-src,style-srcorimg-srcchanges are needed: the launcher's styles use constructable stylesheets, whichstyle-srcdoesn't block.http Content-Security-Policy: script-src 'self' https://atria.bemagic.online; frame-src https://atria.bemagic.online;Using nonces with
'strict-dynamic'? Add your nonce to the Atria tag. The loader never injects other scripts.html <script nonce="{{ nonce }}" src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Single-page apps and page transitions
#Client-side routing, view transitions and body swaps are handled for you.
Load the script once, in your root layout or
index.html. Route changes in React, Vue, Svelte, Angular and friends don't touch it.If a framework replaces the whole
<body>(Turbo, htmx boost, Astro view transitions, Livewirewire:navigate, swup, Barba), the launcher puts itself back automatically.On full page loads, the open conversation and the panel state are restored, so visitors pick up where they left off.
Privacy and consent
#What the widget stores, and when it talks to Atria.
Until a visitor opens the chat, the only request to Atria is loading
widget.js. The widget sets no cookies on your site.It stores an anonymous visitor ID in
localStorage, plus the current conversation ID and panel state insessionStorage. A session is created only when the visitor sends a first message, together with the page URL, referrer, browser language and time zone.If your consent banner must approve support tools first, inject the script after consent:
js function loadAtria() { const script = document.createElement("script"); script.src = "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID"; script.defer = true; document.body.appendChild(script); }
Troubleshooting
#The usual suspects, in order.
No launcher at all? Open the console. A CSP error mentioning
script-srcmeans you need to allowhttps://atria.bemagic.online. No error? Check that the tag isn'ttype="module"and wasn't bundled, combined or delayed by an optimizer.Launcher shows, but the panel is blank? Add
https://atria.bemagic.onlinetoframe-src(orchild-src/default-src).The chat says the agent is unavailable? The
agentIdis missing or wrong. Copy the snippet again from Admin → Agents.React hydration warnings in development? Something added nodes to
<body>before hydration. Load Atria after hydration (see your framework guide) and the warning goes away.The launcher covers your own floating button? It sits in the bottom-right corner, above everything else. Move your button, or hide one of them on small screens.
Meta-frameworks
Next.js (App Router)
#Use next/script in the root layout, so the chat loads after hydration on every route.
Add the script to
app/layout.tsx: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://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" strategy="afterInteractive" /> </body> </html> ); }If you set security headers in
next.config.ts, allow Atria:next.config.ts import type { NextConfig } from "next"; const csp = [ "default-src 'self'", "script-src 'self' https://atria.bemagic.online", "frame-src https://atria.bemagic.online", ].join("; "); const nextConfig: NextConfig = { async headers() { return [ { source: "/:path*", headers: [{ key: "Content-Security-Policy", value: csp }], }, ]; }, }; export default nextConfig;Using a nonce-based CSP from
proxy.ts(middleware.tsbefore Next.js 16)? Read the nonce in the layout and pass it on:app/layout.tsx import { headers } from "next/headers"; import Script from "next/script"; export default async function RootLayout({ children }: { children: React.ReactNode }) { const nonce = (await headers()).get("x-nonce") ?? undefined; return ( <html lang="en"> <body> {children} <Script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" strategy="afterInteractive" nonce={nonce} /> </body> </html> ); }
Next.js (Pages Router)
#Load it once in _app with next/script.
Add the script next to your page component:
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://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" strategy="afterInteractive" /> </> ); }
React Router v7 / Remix
#Add it to root.tsx. On React 18, load it after hydration to avoid hydration warnings.
React 19 (the React Router v7 default): put the tag in the document body, after
<Scripts />.app/root.tsx export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> <Scripts /> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer /> </body> </html> ); }React 18 (Remix v2): load it from an effect, so React has hydrated the document first.
app/root.tsx import { useEffect } from "react"; export default function App() { useEffect(() => { if (document.getElementById("atria-script")) return; const script = document.createElement("script"); script.id = "atria-script"; script.src = "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID"; document.body.appendChild(script); }, []); return <Outlet />; }Set CSP headers in
entry.server.tsx(or aheadersexport) and add Atria toscript-srcandframe-src.
Gatsby
#Use Gatsby's <Script> with the post-hydrate strategy.
Wrap every page once in
gatsby-browser:gatsby-browser.tsx import { Script } from "gatsby"; export const wrapPageElement = ({ element }) => ( <> {element} <Script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" strategy="post-hydrate" /> </> );
Nuxt
#Declare the script in nuxt.config.ts, so every page gets it.
Add it to the app head, at the end of the body:
nuxt.config.ts export default defineNuxtConfig({ app: { head: { script: [ { src: "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID", defer: true, tagPosition: "bodyClose", }, ], }, }, });Using
nuxt-security? Add Atria to its CSP:nuxt.config.ts export default defineNuxtConfig({ security: { headers: { contentSecurityPolicy: { "script-src": ["'self'", "'nonce-{{nonce}}'", "https://atria.bemagic.online"], "frame-src": ["https://atria.bemagic.online"], }, }, }, });
SvelteKit
#Add it to src/app.html.
Paste the tag before the closing body tag:
src/app.html <body data-sveltekit-preload-data="hover"> <div style="display: contents">%sveltekit.body%</div> <script nonce="%sveltekit.nonce%" src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>If you use
kit.csp, add Atria to the directives:svelte.config.js const config = { kit: { csp: { directives: { "script-src": ["self", "https://atria.bemagic.online"], "frame-src": ["https://atria.bemagic.online"], }, }, }, }; export default config;
Astro
#Add it to your base layout with is:inline.
is:inlinetells Astro to leave the tag exactly as written:src/layouts/Layout.astro <html lang="en"> <body> <slot /> <script is:inline src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body> </html>
Angular
#Add it to src/index.html, not to a component template.
Paste the tag before the closing body tag:
src/index.html <body> <app-root></app-root> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>Loading it from code (for example, after consent)? Run it outside the Angular zone, so the widget's events don't trigger change detection:
ts this.ngZone.runOutsideAngular(() => { const script = document.createElement("script"); script.src = "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID"; document.body.appendChild(script); });
Qwik City
#Add the tag to src/root.tsx.
Paste it inside the body:
src/root.tsx <body lang="en"> <RouterOutlet /> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer /> </body>
SolidStart
#Add the tag to the server entry document.
Paste it after the app root:
src/entry-server.tsx <body> <div id="app">{children}</div> {scripts} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer /> </body>
UI libraries & SPAs
React (Vite, Create React App)
#Single-page apps only need the tag in index.html.
Paste it before the closing body tag (
public/index.htmlin Create React App):index.html <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>Need to load it conditionally? Use an effect, and don't remove it on cleanup:
tsx useEffect(() => { if (!user || document.getElementById("atria-script")) return; window.AtriaSettings = { visitor: { id: user.id, name: user.name } }; const script = document.createElement("script"); script.id = "atria-script"; script.src = "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID"; document.body.appendChild(script); }, [user]);
Vue
#Add the tag to index.html in Vite projects.
Paste it before the closing body tag:
index.html <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
Svelte (Vite)
#Add the tag to index.html.
Paste it before the closing body tag:
index.html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Solid, Preact and Lit
#Any Vite-based app works the same way: one tag in index.html.
Paste it before the closing body tag:
index.html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Ember
#Add the tag to app/index.html.
Paste it after
{{content-for "body-footer"}}:app/index.html {{content-for "body-footer"}} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Ionic and Capacitor
#Works in hybrid apps through the WebView, with the same index.html tag.
Paste it before the closing body tag of
src/index.html(Angular) orindex.html(React and Vue):index.html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Static sites & docs
Plain HTML, jQuery and Alpine.js
#Paste the tag on every page, before </body>.
That's the whole install:
index.html <!doctype html> <html lang="en"> <body> <!-- your page --> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body> </html>
Hugo, Jekyll and Eleventy
#Add the tag to your base layout.
Hugo:
layouts/_default/baseof.html {{ block "main" . }}{{ end }} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>Jekyll:
_layouts/default.html {{ content }} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>Eleventy:
_includes/base.njk {{ content | safe }} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
Docusaurus
#Register the script in the site config.
Add it to
scripts:docusaurus.config.ts const config = { scripts: [{ src: "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID", defer: true }], }; export default config;
VitePress
#Add the script to the site head.
Add it to
head:.vitepress/config.ts import { defineConfig } from "vitepress"; export default defineConfig({ head: [["script", { src: "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID", defer: "" }]], });
Server-rendered
Ruby on Rails (with Turbo)
#Add the tag to your application layout.
Put it in
<head>, so Turbo loads it once and keeps it across visits:app/views/layouts/application.html.erb <head> <%= javascript_include_tag "https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID", defer: true, nonce: true %> </head>Allow Atria in your CSP initializer:
config/initializers/content_security_policy.rb Rails.application.configure do config.content_security_policy do |policy| policy.script_src :self, "https://atria.bemagic.online" policy.frame_src "https://atria.bemagic.online" end end
Laravel (Blade, Livewire, Inertia)
#Add the tag to your main Blade layout.
Blade and Livewire:
resources/views/layouts/app.blade.php <head> <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer data-navigate-once></script> </head>Inertia: add it to the root view, outside
@inertia:resources/views/app.blade.php <body> @inertia <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
Django and Flask
#Add the tag to your base template.
Paste it before the closing body tag:
templates/base.html {% block content %}{% endblock %} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>With
django-csp4, allow Atria in settings:settings.py CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "script-src": ["'self'", "https://atria.bemagic.online"], "frame-src": ["https://atria.bemagic.online"], }, }
Phoenix (LiveView)
#Add the tag to the root layout.
Paste it before the closing body tag:
lib/my_app_web/components/layouts/root.html.heex {@inner_content} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
ASP.NET Core (MVC, Razor Pages, Blazor)
#Add the tag to the shared layout or the Blazor host page.
MVC and Razor Pages use
Views/Shared/_Layout.cshtml(orPages/Shared/_Layout.cshtml). Blazor usesComponents/App.razororwwwroot/index.html.Views/Shared/_Layout.cshtml @RenderBody() <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
Website builders & CMS
WordPress
#Enqueue the script from your theme, or paste it with a header and footer plugin.
With a plugin such as WPCode, add the tag as a site-wide footer snippet. Or enqueue it from your child theme:
functions.php add_action('wp_enqueue_scripts', function () { wp_enqueue_script( 'atria', 'https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID', [], null, ['strategy' => 'defer', 'in_footer' => true] ); });
Shopify
#Add the tag to theme.liquid.
In Online Store → Themes → … → Edit code, open
layout/theme.liquidand paste the tag before</body>:layout/theme.liquid {{ content_for_layout }} <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script> </body>
Webflow
#Use site-wide custom code.
In Site settings → Custom code, paste the tag in Footer code, save, and publish.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Wix
#Use Settings → Custom code.
Click Add custom code, paste the tag, choose All pages and Load code once, and place it in Body - end.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Squarespace
#Use Code Injection.
In Website → Pages → Website tools → Code Injection, paste the tag in Footer and save.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Framer
#Use the site's custom code settings.
In Site settings → General → Custom Code, paste the tag in End of <body> tag, then publish.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Ghost
#Use Code injection.
In Settings → Code injection, paste the tag in Site footer and save.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
HubSpot CMS
#Use the site footer HTML.
In Settings → Website → Pages, paste the tag in Site footer HTML and save.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>
Google Tag Manager
#Add a Custom HTML tag that fires on all pages.
Create a Custom HTML tag with the snippet and the All Pages trigger (or your consent trigger), then publish the container.
html <script src="https://atria.bemagic.online/widget.js?agentId=agt_YOUR_AGENT_ID" defer></script>